Open source May 2026
Laraterms

Laraterms

Laraterms

Laraterms is a polymorphic taxonomy system for Laravel: tags, categories or any classification, defined in config and attachable to any model, with per-tenant isolation, translations, hierarchy and cross-locale search built in.

Two tables, no schema opinion beyond them.

Installation

composer require edulazaro/laraterms
php artisan vendor:publish --tag=laraterms-config
php artisan vendor:publish --tag=laraterms-migrations
php artisan migrate

Defining taxonomies

A taxonomy is a named kind of classification declared in config/laraterms.php. The file ships with tags and categories to copy.

'taxonomies' => [
    'tags' => [
        'hierarchical'        => false,
        'max_terms_per_model' => null,
        'scope'               => 'tenant',
    ],
    'categories' => [
        'hierarchical'        => true,
        'max_terms_per_model' => 1,
        'scope'               => 'tenant',
    ],
],

max_terms_per_model is enforced: attaching a second category when the limit is one throws a TooManyTermsException rather than silently allowing it.

Attaching terms

Add the HasTerms trait and the model can carry terms in any taxonomy. Attaching is find-or-create: pass a label and the term is created the first time and reused after.

use EduLazaro\Laraterms\Concerns\HasTerms;

class Post extends Model { use HasTerms; }

$post->attachTerm('Laravel', 'tags');
$post->syncTerms(['Laravel', 'Vue'], 'tags');  // replaces tags, leaves other taxonomies alone
$post->termsIn('tags');                        // read them back

Querying

Post::whereHasTerm('laravel', 'tags')->get();
Post::whereHasAllTerms(['laravel', 'tutorial'], 'tags')->get();
Post::whereInTaxonomy('categories')->get();

Per-tenant isolation

By default a taxonomy is tenant-scoped: each workspace has its own set of terms and they never bleed across. Tell Laraterms how to resolve a model's scope once:

Laraterms::resolveScopeUsing(fn ($model) => $model->organization);

A term created in one organization is invisible to another, and the same label in two organizations is two different terms.

Taxonomies that are genuinely shared (languages, countries) opt out with 'scope' => 'global'.

Translations

Each label has a canonical column plus a translations column in Spatie's format:

{"en": "...", "es": "..."}

The name accessor resolves to the active locale, then a fallback, then the canonical value.

A search_text column is kept current across every locale, so a keyword search matches a term even when the visitor's active language differs from the one they typed in.

Hierarchy

A hierarchical taxonomy gives a real tree, read in a single query, with ancestors and breadcrumbs.

$tree = TermTree::for('categories');
$term->breadcrumb(' > ');   // "Tech > Web > Laravel"

built and maintained by Edu Lazaro · MIT license