whilesmart/eloquent-taxonomy

Polymorphic tags and hierarchical categories for Laravel applications

Maintainers

Package info

github.com/whilesmartphp/eloquent-taxonomy

pkg:composer/whilesmart/eloquent-taxonomy

Statistics

Installs: 1

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 1

dev-main 2026-03-16 00:32 UTC

This package is auto-updated.

Last update: 2026-03-16 00:32:51 UTC


README

Polymorphic tags and hierarchical categories for Laravel applications.

Features

  • Flat Tags: Typed, polymorphic many-to-many tags attachable to any model
  • Hierarchical Categories: Nested parent/child tree with ancestors and descendants
  • Polymorphic Relationships: Works with any Eloquent model via traits
  • Typed Namespaces: Separate tag/category namespaces with the type field (e.g. priority, skill, blog)
  • Query Scopes: withAnyTag, withAllTags, inCategory, inAnyCategory
  • Customizable Models: Override Tag and Category models via config
  • Middleware Hooks: Before/after hooks on all controller actions
  • Configurable Routes: Enable/disable, set prefix and middleware
  • Laravel 10+, 11+, 12+ Support

Installation

composer require whilesmart/eloquent-taxonomy

Publish Configuration (Optional)

php artisan vendor:publish --tag=taxonomy-config

Run Migrations

php artisan migrate

Quick Start

Tags

Add the HasTags trait to any model:

use Whilesmart\Taxonomy\Traits\HasTags;

class Post extends Model
{
    use HasTags;
}

Attach, detach, and query tags:

$post->attachTag('laravel');
$post->attachTags(['laravel', 'php', 'vue']);
$post->detachTag('vue');
$post->syncTags(['laravel', 'react']);

$post->hasTag('laravel');           // true
$post->hasAnyTag(['vue', 'react']); // true

// Typed tags (namespaced)
$post->attachTag('urgent', 'priority');
$post->tagsOfType('priority');

// Query scopes
Post::withAnyTag(['laravel', 'vue'])->get();
Post::withAllTags(['laravel', 'php'])->get();

Categories

Add the HasCategories trait to any model:

use Whilesmart\Taxonomy\Traits\HasCategories;

class Article extends Model
{
    use HasCategories;
}

Create hierarchical categories:

use Whilesmart\Taxonomy\Models\Category;

$tech = Category::create(['name' => 'Technology', 'slug' => 'technology']);
$php = Category::create(['name' => 'PHP', 'slug' => 'php', 'parent_id' => $tech->id]);
$laravel = Category::create(['name' => 'Laravel', 'slug' => 'laravel', 'parent_id' => $php->id]);

Attach and query categories:

$article->attachCategory($tech);
$article->attachCategory('php'); // by slug
$article->detachCategory($tech);
$article->syncCategories([$tech->id, $php->id]);

$article->hasCategory('php');   // true

// Query scopes
Article::inCategory('technology')->get();
Article::inAnyCategory(['technology', 'science'])->get();

Navigate the tree:

$tech->children;         // [PHP]
$php->parent;            // Technology
$laravel->ancestors();   // [Technology, PHP]
$tech->descendants;      // recursive children
$tech->hasChildren();    // true
$tech->isRoot();         // true

// Get full tree (roots with nested descendants)
Category::tree();                // all roots
Category::tree('blog');          // roots of type 'blog'

Using Both

A model can use both traits:

class Product extends Model
{
    use HasTags, HasCategories;
}

Configuration

// config/taxonomy.php
return [
    'models' => [
        'tag' => \Whilesmart\Taxonomy\Models\Tag::class,
        'category' => \Whilesmart\Taxonomy\Models\Category::class,
    ],

    'register_routes' => true,
    'route_prefix' => '',
    'route_middleware' => ['auth:sanctum'],

    'middleware_hooks' => [
        // App\Hooks\TaxonomyHook::class,
    ],
];

Custom Models

Extend the base models and update config:

class Tag extends \Whilesmart\Taxonomy\Models\Tag
{
    // custom logic
}
// config/taxonomy.php
'models' => [
    'tag' => App\Models\Tag::class,
],

Middleware Hooks

Implement MiddlewareHookInterface to intercept controller actions:

use Whilesmart\Taxonomy\Interfaces\MiddlewareHookInterface;

class TaxonomyHook implements MiddlewareHookInterface
{
    public function before(Request $request, string $action): ?Request
    {
        // modify request or return null to skip
        return $request;
    }

    public function after(Request $request, JsonResponse $response, string $action): JsonResponse
    {
        // modify response
        return $response;
    }
}

API Routes

When routes are enabled:

GET    /tags                    List tags (filter: ?type=)
POST   /tags                    Create tag
DELETE /tags/{id}               Delete tag

GET    /categories              List categories (filter: ?type=, ?tree=true)
POST   /categories              Create category
GET    /categories/{id}         Show category with children
PUT    /categories/{id}         Update category
DELETE /categories/{id}         Delete category

Database Schema

Tags

Column Type Description
name string Display name
slug string URL-friendly identifier
type string (nullable) Namespace for grouping
color string (nullable) Hex color for UI
metadata JSON (nullable) Flexible data
sort_order integer Display ordering

Unique constraint: (slug, type)

Categories

Column Type Description
parent_id FK (nullable) Self-referencing for hierarchy
name string Display name
slug string URL-friendly identifier
type string (nullable) Namespace for grouping
description text (nullable) Description
icon string (nullable) Icon identifier
color string (nullable) Hex color for UI
metadata JSON (nullable) Flexible data
sort_order integer Display ordering

Unique constraint: (slug, type)

Testing

make test       # Run tests via Docker
make pint       # Run code formatter
make check      # Run all checks (pint + tests)

Requirements

  • PHP 8.2+
  • Laravel 10.0, 11.0, or 12.0

License

MIT License

Credits

Developed by the Whilesmart Team