laravel-terms maintained by aliziodev
Laravel Terms
Lightweight, flat taxonomy package for Laravel. Attach reusable terms — tags, categories, brands, colors, sizes, or any custom type — to any Eloquent model via a polymorphic pivot, without nested-set complexity.
Features
- Flat term model — no nested sets, no adjacency lists; one simple
termstable - Polymorphic pivot — attach terms to any model with a single trait
- Type-based scoping — built-in enum types (
tag,category,brand,color,size) plus arbitrary string types - Context on pivot — group term attachments by an optional context string (e.g.
primary,sidebar), with context-aware sync and detach - Configurable morph key —
numeric(default),uuid, orulid - Auto slug generation — slugs derived from names, unique per type
hasTerm()— fast boolean existence check without loading the relationwhereHasTerms()— scope for AND / OR multi-term filteringTerm::ordered()— scope to sort terms bysort_order- Minimal surface area — manager, trait, one model, one migration
Requirements
| Laravel | PHP |
|---|---|
| 11.x | 8.2+ |
| 12.x | 8.2+ |
| 13.x | 8.3+ |
Installation
composer require aliziodev/laravel-terms
Run the installer (publishes config + migration, optionally migrates):
php artisan terms:install
Or publish manually:
php artisan vendor:publish --tag=terms-config
php artisan vendor:publish --tag=terms-migrations
php artisan migrate
Configuration
config/terms.php — published to your app:
return [
'table_names' => [
'terms' => 'terms',
'termables' => 'termables',
],
// Supported: "numeric" (default), "uuid", "ulid"
// Must match the primary key type of models using HasTerms.
'morph_type' => 'numeric',
// Built-in types. Arbitrary strings are also accepted at runtime.
'types' => ['tag', 'category', 'brand', 'color', 'size'],
// Swap in your own model if you need to extend Term.
'model' => \Aliziodev\LaravelTerms\Models\Term::class,
'slugs' => [
'generate' => true,
'regenerate_on_update' => false,
],
];
Usage
1. Add the trait to your model
use Aliziodev\LaravelTerms\Traits\HasTerms;
class Product extends Model
{
use HasTerms;
}
If your model uses UUID or ULID primary keys, set morph_type in config/terms.php to match.
2. Attach, sync, and detach terms
use Aliziodev\LaravelTerms\Enums\TermType;
// Sync — replaces all existing tags with the new list
$product->syncTerms(['new-arrival', 'sale'], TermType::Tag);
// Attach — adds without removing existing terms
$product->attachTerms(['nike'], TermType::Brand);
// Detach specific terms
$product->detachTerms(['sale'], TermType::Tag);
// Detach all terms of a type
$product->detachTerms([], TermType::Tag);
// Detach everything
$product->detachTerms();
// Detach within a specific context only (other contexts remain untouched)
$product->detachTerms([], TermType::Tag, 'homepage');
$product->detachTerms(['sale'], TermType::Tag, 'homepage');
Custom string types work without any configuration:
$product->syncTerms(['waterproof', 'breathable'], 'material');
3. Query
// All terms attached to the model
$product->terms;
// Terms of a specific type
$product->termsOfType(TermType::Tag)->get();
// Fast boolean existence check — no collection loaded
$product->hasTerm(TermType::Brand, 'nike'); // true / false
// Find products that have a specific term
Product::whereHasTerm(TermType::Brand, 'nike')->get();
// Models that have ALL of the given terms (AND — default)
Product::whereHasTerms(TermType::Tag, ['new', 'sale'])->get();
// Models that have ANY of the given terms (OR)
Product::whereHasTerms(TermType::Tag, ['new', 'sale'], 'or')->get();
4. Context
Attach the same term under different contexts (e.g. display slots):
$product->attachTerms(['red'], TermType::Color, 'primary');
$product->attachTerms(['blue'], TermType::Color, 'secondary');
// Syncing within a context leaves other contexts untouched
$product->syncTerms(['yellow'], TermType::Color, 'primary');
// Filter by context on the pivot
$product->termsOfType(TermType::Color)->wherePivot('context', 'primary')->get();
5. Facade
use Aliziodev\LaravelTerms\Facades\Terms;
$term = Terms::findOrCreate('Summer', TermType::Tag);
$terms = Terms::findOrCreateMany(['new', 'sale'], TermType::Tag);
Terms::attach($product, ['red'], TermType::Color);
Terms::sync($product, ['blue'], TermType::Color);
Terms::detach($product, ['red'], TermType::Color);
6. Term model scopes
// Filter by type or slug
Term::query()->type('tag')->get();
Term::query()->slug('new-arrival')->first();
// Sort by sort_order (asc by default)
Term::query()->type('tag')->ordered()->get();
Term::query()->ordered('desc')->get();
Extending the Term model
Publish the config and swap model:
// config/terms.php
'model' => App\Models\Term::class,
namespace App\Models;
class Term extends \Aliziodev\LaravelTerms\Models\Term
{
// Add relations, scopes, or accessors here
}
Differences from laravel-taxonomy
| Feature | laravel-terms | laravel-taxonomy |
|---|---|---|
| Hierarchy | None (flat) | Nested set / adjacency list |
| Term ordering | sort_order column |
Full tree ordering |
| Pivot context | Yes | Varies |
| Migration complexity | 2 tables | 2 tables |
| Footprint | Minimal | Feature-rich |
Use laravel-terms when you need simple, flat labels. Use laravel-taxonomy when you need parent–child term trees.
Testing
composer test
License
MIT — see LICENSE.