wuwx / laravel-taxonomy
A Drupal-inspired taxonomy package for Laravel models.
Requires
- php: ^8.2
- illuminate/database: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- kalnoy/nestedset: ^6.0
- spatie/laravel-package-tools: ^1.93
- spatie/laravel-sluggable: ^3.8
- spatie/laravel-translatable: ^6.11
Requires (Dev)
- laravel/pint: ^1.24
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.0
README
A Drupal-inspired taxonomy package for Laravel applications. It provides:
- Vocabularies via
Taxonomy— group terms into categories, tags, locations, etc. - Hierarchical terms via
Term— powered bykalnoy/nestedsetfor efficient tree queries - Polymorphic assignment — attach terms to any Eloquent model
- Rich query scopes —
withAnyTerms,withAllTerms,withoutTerms,byTaxonomies - Translations —
nameanddescriptionare translatable viaspatie/laravel-translatable - Slug auto-generation — powered by
spatie/laravel-sluggablewith scoped uniqueness - Events —
TermAttached,TermDetached,TermsSynceddispatched automatically - Pivot data —
orderandmetadataon the pivot table - Artisan commands —
taxonomy:list,taxonomy:tree,taxonomy:create-term
Installation
composer require wuwx/laravel-taxonomy php artisan laravel-taxonomy:install
If you prefer manual setup:
php artisan vendor:publish --tag=laravel-taxonomy-config php artisan vendor:publish --tag=laravel-taxonomy-migrations php artisan migrate
Quick Start
Attach the HasTaxonomyTerms trait to a model:
use Wuwx\LaravelTaxonomy\Traits\HasTaxonomyTerms; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasTaxonomyTerms; }
Create a vocabulary and some terms:
use Wuwx\LaravelTaxonomy\Models\Taxonomy; $topics = Taxonomy::query()->create(['name' => 'Topics']); $php = $topics->createTerm(['name' => 'PHP']); $laravel = $topics->createTerm(['name' => 'Laravel'], parent: $php);
Assign and query:
$post->attachTerm($php); $post->attachTerms(['php', 'laravel'], taxonomy: 'topics'); Post::withAnyTerms(['php', 'laravel'], taxonomy: 'topics')->get();
Taxonomies And Terms
Create a taxonomy (slug is auto-generated if omitted):
$topics = Taxonomy::query()->create([ 'name' => 'Topics', 'description' => 'Development topics', 'is_hierarchical' => true, ]);
Create root and child terms:
$backend = $topics->createTerm(['name' => 'Backend']); $php = $topics->createTerm(['name' => 'PHP'], parent: $backend); $laravel = $topics->createTerm(['name' => 'Laravel'], parent: $php);
Look up terms:
$topics->findTermBySlug('php'); $topics->rootTerms();
Slugs are auto-generated and unique — within the same taxonomy, duplicate names produce php, php-1, php-2, etc. Taxonomy slugs are globally unique.
If a taxonomy is not hierarchical, creating child terms will throw an InvalidArgumentException.
Assigning Terms To Models
$post->attachTerm($php); $post->attachTerm('laravel', taxonomy: 'topics'); $post->attachTerms([$php, $laravel]); $post->syncTerms(['php', 'laravel'], taxonomy: 'topics'); $post->syncTerms(['php'], taxonomy: 'topics', detaching: false); $post->detachTerm('laravel', taxonomy: 'topics'); $post->detachTerms(['php', 'laravel'], taxonomy: 'topics'); $post->detachAllTerms();
String-based term resolution requires a taxonomy:
$post->attachTerm('laravel', taxonomy: 'topics'); $post->attachTerms(['php', 'laravel'], taxonomy: $topics);
Pivot Data
Attach terms with extra pivot data (order and metadata columns):
$post->attachTerm($php, pivot: ['order' => 1, 'metadata' => json_encode(['primary' => true])]); $post->attachTerms([$php, $laravel], pivot: ['order' => 5]); $post->terms->first()->pivot->order; // 1 $post->terms->first()->pivot->metadata; // '{"primary":true}'
Checking Attached Terms
$post->hasTerm($php); $post->hasTerm('laravel', taxonomy: 'topics'); $post->hasAnyTerms(['php', 'go'], taxonomy: 'topics'); $post->hasAllTerms(['php', 'laravel'], taxonomy: 'topics');
Unknown terms resolve to false.
Querying Models By Terms
Post::whereHasTerm('laravel', taxonomy: 'topics')->get(); Post::withAnyTerms(['php', 'laravel'], taxonomy: 'topics')->get(); Post::withAllTerms(['php', 'laravel'], taxonomy: 'topics')->get(); Post::withoutTerms(['deprecated'], taxonomy: 'statuses')->get(); Post::withoutAnyTerms()->get();
Multi-Taxonomy Filtering
Filter by multiple vocabularies at once — AND between vocabularies, OR within:
Post::byTaxonomies([ 'topics' => ['php', 'laravel'], // has php OR laravel 'cities' => ['shanghai'], // AND has shanghai ])->get();
Translations
name and description are translatable via spatie/laravel-translatable:
$topics = Taxonomy::query()->create([ 'name' => ['en' => 'Topics', 'zh' => '主题'], 'description' => ['en' => 'Blog topics', 'zh' => '博客主题'], ]); $php = $topics->createTerm([ 'name' => ['en' => 'PHP', 'zh' => 'PHP 编程'], ]); app()->setLocale('zh'); $topics->name; // '主题' $php->name; // 'PHP 编程'
Single-language usage works as before — just pass a plain string:
$topics = Taxonomy::query()->create(['name' => 'Topics']);
Working With Trees
Term uses kalnoy/nestedset internally, so all tree operations are single-query, not recursive.
$laravel->parent; $php->children()->get(); $backend->descendants()->get(); $laravel->ancestors()->get(); $laravel->siblings()->get(); $backend->isRoot(); $laravel->isLeaf(); $backend->isAncestorOf($laravel); $laravel->isDescendantOf($backend); $backend->ancestors()->count(); // 0 $php->ancestors()->count(); // 1 $laravel->ancestors()->count(); // 2
Build tree structures for menus, navigation, or selects:
$tree = $topics->toTree(); // nested with children relations $flatTree = $topics->toFlatTree(); // flat list with computed depth attribute
Events
All attach/detach/sync operations dispatch events:
| Operation | Event |
|---|---|
attachTerm / attachTerms |
TermAttached |
detachTerm / detachTerms / detachAllTerms |
TermDetached |
syncTerms |
TermsSynced |
use Wuwx\LaravelTaxonomy\Events\TermAttached; Event::listen(TermAttached::class, function (TermAttached $event) { // $event->model — the Eloquent model // $event->termIds — array of attached term IDs });
TermsSynced also includes $event->changes with attached, detached, and updated arrays.
Artisan Commands
php artisan taxonomy:list # list all taxonomies with term counts php artisan taxonomy:tree topics # tree view of a taxonomy's terms php artisan taxonomy:create-term topics "PHP" # create a term php artisan taxonomy:create-term topics "Laravel" --parent=php # create a child term
Configuration
The default config file is config/laravel-taxonomy.php:
return [ 'table_names' => [ 'taxonomies' => 'taxonomies', 'terms' => 'taxonomy_terms', 'morph_pivot' => 'termables', ], 'models' => [ 'taxonomy' => Taxonomy::class, 'term' => Term::class, ], ];
Both Taxonomy and Term support Route Model Binding via slug by default.
Testing
vendor/bin/pint --test vendor/bin/phpunit