whilesmart / eloquent-taxonomy
Polymorphic tags and hierarchical categories for Laravel applications
dev-main
2026-03-16 00:32 UTC
Requires
- php: ^8.2
- laravel/framework: ^10.0|^11.0|^12.0
Requires (Dev)
- fakerphp/faker: ^1.24
- laravel/pint: ^1.25
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
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
typefield (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