Laravel Taxonomy

repository·master·Indexed 42 days ago

https://github.com/aliziodev/laravel-taxonomy

A Laravel package for managing categories, tags, and hierarchical structures using a nested set model. It provides a single table for all terms, a polymorphic pivot table for attachments, and a high-level facade for term management, including high-performance bulk imports and multi-tenancy support.

Tokens
20.2K
Snippets
60
Records
69
Agent score
47%

What's inside Laravel Taxonomy

  1. Understand type validation behavior changes

    master

    In v2.11.0, methods like syncTaxonomiesOfType() and attachTaxonomiesOfType() no longer run through the application's global scopes when verifying taxonomy IDs. This ensures that shared rows (visible to all tenants) are correctly identified even in multi-tenant setups.

    Security Note: If you rely on global scopes to prevent users from attaching taxonomies belonging to other tenants, you must now manually validate the IDs (e.g., using Laravel's Rule::exists()) before passing them to taxonomy methods.

  2. Avoid custom relationship names in taxonomy methods

    master

    As of v2.11.0, passing a custom relationship name as the second argument to taxonomy methods (e.g., $product->attachTaxonomies($ids, 'categories')) is deprecated and will be removed in v3.0.

    The underlying schema only supports the default taxonomable morph columns. To separate different types of taxonomies (like categories vs. tags), use taxonomy types instead. If you need multiple roles for a single type, add a relation column to your pivot table and use wherePivot() to filter.

  3. Distinguish between the Taxonomy Facade and Taxonomy Model

    master

    The package provides two distinct classes that are not interchangeable. Using the wrong one is a common mistake:

    1. Aliziodev\LaravelTaxonomy\Facades\Taxonomy: A proxy for TaxonomyManager. Use this for high-level operations like create(), find(), findBySlug(), and tree operations (tree(), flatTree()). It does not support Eloquent builder methods like where() or withCount().
    2. Aliziodev\LaravelTaxonomy\Models\Taxonomy: The Eloquent model. Use this for database queries and building complex filters.

    Note: The taxonomy model does not have a models relationship. To count associated records, you must count from the model side or aggregate via the pivot table.

    use Aliziodev\LaravelTaxonomy\Facades\Taxonomy;            // create, find, tree
    use Aliziodev\LaravelTaxonomy\Models\Taxonomy as TaxonomyModel;  // query building
  4. Manage Taxonomy Hierarchies and Trees

    master

    Taxonomies support hierarchies using both parent_id and a nested set (lft/rgt/depth).

    Navigation & Relationships:

    • $node->parent: Relationship to the parent.
    • $node->children: Relationship to children (sorted by sort_order).
    • $node->ancestors(): Follows parent_id (reliable even if nested set is corrupted).
    • $node->descendants(): Depth-first traversal (one query per level).
    • $node->getAncestors(): Uses nested set (fastest, one query).
    • $node->getDescendants(): Uses nested set (fastest, one query).
    • $node->getSiblings(): Relationship to siblings.
    • $node->getChildren(): Relationship to children.
    • $node->isAncestorOf($other) / $node->isDescendantOf($other): Boolean checks.
    • $node->getLevel(): Returns depth (0 for root).
    • $node->path: Returns string path (e.g., "Electronics > Smartphones").
    • $node->full_slug: Returns full slug path (e.g., "electronics/smartphones").
    • $node->moveToParent($newParentId): Moves node to a new parent.

    Tree Generation:

    • Taxonomy::tree(TaxonomyType::Type): Returns root with one level of children.
    • Taxonomy::flatTree(TaxonomyType::Type): Returns a flat list with depth populated.
    • Taxonomy::getNestedTree(TaxonomyType::Type): Returns full depth with children_nested and tree_depth.

    Important: If you modify the database directly (outside the model), the nested set values (lft/rgt) might become out of sync. In such cases, use ancestors()/descendants() which rely on parent_id, or run the taxonomy:rebuild-nested-set command. Also, always refresh() a model instance if you have added children to it via a different process before calling nested set methods like getDescendants().

    // Fast nested set retrieval
    $ancestors = $node->getAncestors();
    $descendants = $node->getDescendants();
    
    // Reliable parent_id traversal
    $ancestors = $node->ancestors();
    $descendants = $node->descendants();
    
    // Generating trees
    $tree = Taxonomy::getNestedTree(TaxonomyType::Category);
  5. Store and Query SEO Metadata in Taxonomies

    master

    The meta column on the Taxonomy model is a JSON column. This is ideal for storing term-specific SEO data like titles and descriptions. You can query these JSON fields directly using Laravel's arrow syntax.

    // Creating a taxonomy with SEO metadata
    Taxonomy::create([
        'name' => 'News',
        'type' => TaxonomyType::Category,
        'meta' => [
            'seo' => [
                'title'       => 'Latest News and Updates',
                'description' => 'Stay current with our announcements.',
            ],
        ],
    ]);
    
    // Accessing metadata
    $title = $category->meta['seo']['title'] ?? $category->name;
    
    // Querying by metadata
    TaxonomyModel::where('meta->seo->title', '!=', null)->get();
  6. Distinguish between Taxonomy Facade and Taxonomy Model

    master

    The package provides two primary ways to interact with taxonomies, which serve different purposes:

    1. Aliziodev\LaravelTaxonomy\Facades\Taxonomy: A proxy for the manager. Use this for administrative tasks like creating, finding, or retrieving trees (e.g., Taxonomy::create(), Taxonomy::getNestedTree()). It does not support Eloquent query builder methods like where().
    2. Aliziodev\LaravelTaxonomy\Models\Taxonomy: The Eloquent model. Use this for building queries against taxonomy records.

    They are not interchangeable.

  7. Configure Cache Scoping for Multitenancy

    master

    By default, taxonomy tree caches are global. In a multitenant application, you must define a cache scope to prevent one tenant from seeing another's taxonomy tree.

    Option 1: Using a Closure (via Service Provider) Register a resolver in your AppServiceProvider::boot() method:

    TaxonomyManager::resolveCacheScopeUsing(fn () => tenant()?->getKey());

    Option 2: Using an Invokable Class (Recommended for Config Caching) Point the taxonomy.cache.scope configuration key to an invokable class. This is safer for php artisan config:cache than using a closure.

    class TenantCacheScope
    {
        public function __invoke(): ?string
        {
            return tenant()?->getKey();
        }
    }

    Important: Do not wrap taxonomy tree calls (like tree()) in your own Cache::remember() calls. The package uses versioned keys for invalidation; a manual wrapper will not see these invalidations and will serve stale data.

    // In AppServiceProvider
    use Aliziodev\
    LaravelTaxonomy\\TaxonomyManager;
    
    TaxonomyManager::resolveCacheScopeUsing(fn () => tenant()?->getKey());
  8. Quick Start: Create and Attach Taxonomies

    master

    You can create taxonomies using the Taxonomy facade and attach them to models using the HasTaxonomy trait.

    1. Create a taxonomy: Use Taxonomy::create() providing a name and a TaxonomyType.
    2. Attach to a model: Add the HasTaxonomy trait to your model to enable attachment methods like attachTaxonomies().
    use Aliziodev\LaravelTaxonomy\Facades\Taxonomy;
    use Aliziodev\LaravelTaxonomy\Enums\TaxonomyType;
    use Aliziodev\LaravelTaxonomy\Traits\HasTaxonomy;
    
    // 1. Create taxonomies
    $electronics = Taxonomy::create([
        'name' => 'Electronics',
        'type' => TaxonomyType::Category,
        'meta' => ['icon' => 'devices'],
    ]);
    
    $phones = Taxonomy::create([
        'name'      => 'Smartphones',
        'type'      => TaxonomyType::Category,
        'parent_id' => $electronics->id,
    ]);
    
    // 2. Attach to a model
    class Product extends Model
    {
        use HasTaxonomy;
    }
    
    $product = new Product();
    $product->attachTaxonomies([$phones->id]);
    
    // 3. Accessing
    $product->taxonomies; // returns all attached terms
  9. Bulk import taxonomy terms

    master

    For large datasets (seeders or imports), use Taxonomy::bulkCreate() instead of looping over create(). bulkCreate() is significantly faster as it minimizes queries and handles slug generation and nested set population in batches.

    Important Considerations:

    • No Model Events: bulkCreate() does not fire Eloquent model events. If your logic relies on observers, use create() instead.
    • Avoid Raw DB Inserts: Never use DB::table('taxonomies')->insert(). This bypasses model hooks and leaves the nested set (lft/rgt/depth) null, making the tree unusable. If you must use raw SQL, you must call Taxonomy::rebuildNestedSet($type) immediately after.
    Taxonomy::bulkCreate([
        ['name' => 'Electronics', 'type' => TaxonomyType::Category],
        ['name' => 'Fiction', 'type' => TaxonomyType::Category, 'parent_id' => $booksId],
    ]);
    Taxonomy::bulkCreate([
        ['name' => 'Electronics', 'type' => TaxonomyType::Category],
        ['name' => 'Fiction', 'type' => TaxonomyType::Category, 'parent_id' => $booksId],
    ]);
  10. Implement Data Isolation for Multitenancy

    master

    The package does not include a tenant_id column by default. To isolate taxonomy data per tenant, follow these steps:

    1. Modify the Migration: Add a tenant_id column and update the unique index to include it. The default unique index is ['slug', 'type', 'deleted_at'].
    Schema::table('taxonomies', function (Blueprint $table) {
        $table->dropUnique(['slug', 'type', 'deleted_at']);
        $table->foreignId('tenant_id')->nullable()->index();
        $table->unique(['tenant_id', 'slug', 'type', 'deleted_at']);
    });
    1. Extend the Taxonomy Model: Create your own Taxonomy model that extends the package's base model and applies a Global Scope for the tenant.
    namespace App\
    Models;
    
    use Aliziodev\\LaravelTaxonomy\\Models\\Taxonomy as BaseTaxonomy;
    
    class Taxonomy extends BaseTaxonomy
    {
        protected static function boot(): void
        {
            parent::boot();
            static::addGlobalScope('tenant', function ($query) {
                if ($tenantId = tenant()?->getKey()) {
                    $query->where('tenant_id', $tenantId);
                }
            });
        }
    }
    1. Update Configuration: Point the package to your custom model in the config file:
    'model' => \App\\Models\\Taxonomy::class,

    Warning: The *OfType methods validate IDs without applying global scopes. If you pass an ID belonging to another tenant, it may still be attached. Always validate user input using scoped rules (e.g., Rule::exists()).

  11. Create a nested category tree

    master

    To build a hierarchical structure, use the parent_id attribute when creating taxonomies. The package automatically maintains the lft, rgt, and depth values for you.

    Ensure you set the type to a category type (e.g., TaxonomyType::Category) to distinguish it from flat attributes.

    use Aliziodev\LaravelTaxonomy\Enums\TaxonomyType;
    use Aliziodev\LaravelTaxonomy\Facades\Taxonomy;
    
    $electronics = Taxonomy::create([
        'name' => 'Electronics',
        'type' => TaxonomyType::Category,
        'meta' => ['icon' => 'devices'],
    ]);
    
    $phones = Taxonomy::create([
        'name'      => 'Smartphones',
        'type'      => TaxonomyType::Category,
        'parent_id' => $electronics->id,
    ]);
    
    $android = Taxonomy::create([
        'name'      => 'Android',
        'type'      => TaxonomyType::Category,
        'parent_id' => $phones->id,
    ]);
  12. Perform high-performance bulk imports with bulkCreate()

    master

    When importing large datasets (e.g., from seeders or CSVs), use Taxonomy::bulkCreate() instead of calling create() in a loop. bulkCreate() is significantly faster because it resolves slugs in memory, inserts in chunks, and renumbers the nested set once at the end, rather than performing multiple queries per row.

    Important: bulkCreate() does not fire Eloquent model events. However, the package handles slug generation, nested set calculations, and cache invalidation internally during the bulk process.

    Supported attributes: name (required), type (required), slug, description, parent_id, sort_order, meta, created_at, and updated_at.

    Taxonomy::bulkCreate([
        ['name' => 'Electronics', 'type' => TaxonomyType::Category],
        ['name' => 'Books',       'type' => TaxonomyType::Category, 'sort_order' => 2],
        ['name' => 'Fiction',     'type' => TaxonomyType::Category, 'parent_id' => $booksId],
    ]);