mrnewport / laravel-nestedterms
A super dynamic & extensible Terms + Tags system for Laravel, featuring infinite nesting, dynamic casting, hierarchical slugs, etc.
Requires
- php: ^8.2
- illuminate/database: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-19 22:20:42 UTC
README
Current release: 2.0.0. Supported installations: Laravel 12 (PHP 8.2+) and Laravel 13 (PHP 8.3+). CI verifies supported PHP/Laravel combinations. Earlier Laravel versions should remain on the previous major release.
Laravel 12/13 with bounded component dependencies. Fixed structured values being double-encoded, configurable table membership lookups and custom model relations. Parent traversal now rejects cycles and cross-term parents. Modernized the case-sensitive Pest bootstrap and added regression tests.
composer require mrnewport/laravel-nestedterms:^2.0 composer test # from the package checkout; tests use isolated fixtures
GitHub source and tags are published first. Until the release is indexed on Packagist, add this repository as a Composer VCS repository. Never install test dependencies in your production application's require section.
Run php artisan migrate after upgrading to apply package migrations. Back up application data before normal production migrations.
Existing API reference
Laravel Nested Terms & Tags
A dynamic & extensible system for nested Terms and dynamic-cast Tags in Laravel. This package includes:
- Infinite nesting of Tags via a self-referencing
parent_id. - Hierarchical slugs (e.g.
specifications.bedrooms.5). - Dynamic casting of a Tag’s
valuebased on atypefield (integer,boolean,float,array, etc.). - Polymorphic pivot for attaching Tags to any Eloquent model.
- Config-driven architecture for easy customization.
- Comprehensive tests and a production-ready codebase.
Table of Contents
Requirements
- PHP
^8.1or higher - Laravel
^11.0(or equivalent) - Composer for dependency management
Installation
-
Install via Composer:
composer require mrnewport/laravel-nestedterms
-
Publish the config (optional):
php artisan vendor:publish --provider="MrNewport\LaravelNestedTerms\Providers\NestedTermsServiceProvider" --tag=nestedterms-config -
Migrate:
php artisan migrate
Configuration
config/nestedterms.php (published if desired):
return [ 'terms_table' => 'terms', 'tags_table' => 'tags', 'model_tag_table' => 'model_tag', 'term_model' => \MrNewport\LaravelNestedTerms\Models\Term::class, 'tag_model' => \MrNewport\LaravelNestedTerms\Models\Tag::class, 'allowed_cast_types' => [ 'integer','float','double','boolean','string','array','json','object', ], 'custom_type_map' => [ 'number' => 'integer', ], ];
- Table Names:
terms_table,tags_table,model_tag_table - Model Classes:
term_model&tag_model - Casting:
'allowed_cast_types'&'custom_type_map'
Usage
Creating Terms
use MrNewport\LaravelNestedTerms\Models\Term; $term = Term::create([ 'name' => 'Specifications', 'slug' => 'specifications', // or omit to auto-generate ]);
Creating Tags
use MrNewport\LaravelNestedTerms\Models\Tag; $bedrooms = Tag::create([ 'term_id' => $term->id, 'name' => 'Bedrooms', ]);
Infinite Nesting
Tags can nest via parent_id:
$parent = Tag::create([ 'term_id' => $term->id, 'name' => 'SubItem', ]); $child = Tag::create([ 'term_id' => $term->id, 'parent_id' => $parent->id, 'name' => 'Child Tag', ]); // slug => "specifications.subitem.child-tag" $descendants = $parent->allDescendants(); // includes "Child Tag"
Tag Values & Casting
$tag = Tag::create([ 'term_id' => $term->id, 'name' => 'NumberOfRooms', 'type' => 'integer', 'value' => '5', ]); // Eloquent interprets $tag->value as integer echo $tag->value; // 5
Attaching Tags to Models
Any model can “have tags”:
use MrNewport\LaravelNestedTerms\Traits\HasTags; class Article extends Model { use HasTags; }
$article->attachTags($tagIdOrSlug); $article->detachTags($tagInstance); $article->syncTags([...]); $article->hasTag('bedrooms');
Filtering by Term
$article->tagsByTerm('specifications')->get(); // returns tags whose term->slug == "specifications"
Customization
Custom Table Names
In nestedterms.php:
'terms_table' => 'cms_terms', 'tags_table' => 'cms_tags',
Then re-run php artisan migrate. The included migrations reference these config values.
Custom Models
Define your own Term or Tag classes:
namespace App\Models; use MrNewport\LaravelNestedTerms\Models\Tag as BaseTag; class CustomTag extends BaseTag { protected $fillable = [ 'term_id','parent_id','slug','type','value','name','description','meta','is_active', 'icon','color' ]; public function generateHierarchySlug(): string { $slug = parent::generateHierarchySlug(); return $slug.'-extended'; } }
Then update:
'tag_model' => \App\Models\CustomTag::class,
Testing
A Pest-based suite covers Terms, Tags, and the HasTags trait. Run:
composer test
- TermTest: verifying creation, slug generation, etc.
- TagTest: infinite nesting, dynamic casting, hierarchical slugs.
- HasTagsTraitTest: attaching/detaching tags on a test model.
Contributing
- Fork this repo.
- Create a feature/fix branch.
- Add tests covering changes.
- Submit a Pull Request.
License
Licensed under the MIT license.
Enjoy building infinite nesting, dynamic-cast tags, and configurable terms in your Laravel projects!