Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ancestry Laravel Package

cline/ancestry

Closure table hierarchies for Laravel Eloquent. Manage deep trees (org charts, categories) with O(1) ancestor/descendant queries, fluent APIs, configurable keys/types, events, and snapshots for point-in-time hierarchy state.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require cline/ancestry
    

    Add the trait to your Eloquent model:

    use Ancestry\AncestryTrait;
    
    class Category extends Model
    {
        use AncestryTrait;
    }
    
  2. Migration: Add ancestry column to your table:

    Schema::table('categories', function (Blueprint $table) {
        $table->string('ancestry')->nullable();
    });
    
  3. First Use Case:

    // Create a root node
    $root = Category::create(['name' => 'Root']);
    
    // Create a child
    $child = $root->children()->create(['name' => 'Child']);
    
    // Query descendants
    $descendants = $root->descendants;
    

Where to Look First

  • README.md: Covers basic usage and migration steps.
  • src/AncestryTrait.php: Core trait implementation for reference.
  • tests/: Example test cases for edge scenarios.

Implementation Patterns

Core Workflows

  1. Hierarchy Creation:

    // Add child to existing node
    $parent = Category::find(1);
    $child = $parent->children()->create(['name' => 'New Child']);
    
    // Or create with ancestry string
    $node = Category::create(['name' => 'New Node', 'ancestry' => '1,2']);
    
  2. Querying:

    // Get all descendants (O(1) operation)
    $root->descendants;
    
    // Get all ancestors
    $node->ancestors;
    
    // Check if node is descendant of another
    $node->isDescendantOf($root);
    
    // Get siblings
    $node->siblings;
    
  3. Reorganizing:

    // Move node to new parent
    $node->moveTo($newParent);
    
    // Make node a root (detach from hierarchy)
    $node->makeRoot();
    

Integration Tips

  • Scopes: Extend the trait with custom scopes:

    public function scopeActive($query)
    {
        return $query->where('active', true)->whereHasAncestor(function ($q) {
            $q->where('active', true);
        });
    }
    
  • Events: Listen for hierarchy changes:

    Category::created(function ($model) {
        if ($model->ancestry) {
            event(new HierarchyChanged($model));
        }
    });
    
  • API Responses: Serialize hierarchies efficiently:

    $response->json($node->with(['children' => function ($query) {
        return $query->orderBy('name');
    }]));
    
  • Caching: Cache frequent queries (e.g., root nodes):

    Cache::remember('root_categories', now()->addHours(1), function () {
        return Category::roots()->get();
    });
    

Gotchas and Tips

Common Pitfalls

  1. Circular References:

    • Issue: Moving a node to itself or its descendant creates invalid ancestry strings.
    • Fix: Validate before moving:
      if ($node->isDescendantOf($newParent)) {
          throw new \InvalidArgumentException('Cannot move node to its descendant');
      }
      
  2. Performance with Large Trees:

    • Issue: Deep hierarchies may cause long ancestry strings (max 255 chars by default).
    • Fix: Use ancestry_depth column (add via migration) and limit depth in queries:
      $query->where('ancestry_depth', '<', 5);
      
  3. Race Conditions:

    • Issue: Concurrent writes to the same hierarchy can corrupt ancestry strings.
    • Fix: Use database transactions:
      DB::transaction(function () use ($node, $newParent) {
          $node->moveTo($newParent);
      });
      
  4. Serialization:

    • Issue: Ancestry strings may break JSON serialization if not handled.
    • Fix: Exclude from API responses or cast:
      protected $casts = [
          'ancestry' => 'hidden',
      ];
      

Debugging Tips

  • Validate Ancestry Strings:

    if (!$model->ancestry || !Ancestry::isValid($model->ancestry)) {
        throw new \InvalidArgumentException('Invalid ancestry string');
    }
    
  • Log Hierarchy Changes:

    Category::updated(function ($model) {
        if ($model->isDirty('ancestry')) {
            \Log::debug('Ancestry changed', ['model' => $model->id, 'new_ancestry' => $model->ancestry]);
        }
    });
    

Extension Points

  1. Custom Ancestry Logic: Override getAncestryAttribute or setAncestryAttribute in your model:

    public function getAncestryAttribute($value)
    {
        return $value ? explode(',', $value) : [];
    }
    
  2. Add Metadata: Extend the trait to store additional hierarchy data:

    protected $ancestryMetadata = [];
    
    public function setAncestryMetadata($key, $value)
    {
        $this->ancestryMetadata[$key] = $value;
        $this->save();
    }
    
  3. Custom Query Builder: Extend the query builder for hierarchy-specific methods:

    public function scopePathTo($query, $ancestor)
    {
        return $query->where(function ($q) use ($ancestor) {
            $q->where('ancestry', 'LIKE', $ancestor->ancestry . ',%')
              ->orWhere('id', $ancestor->id);
        });
    }
    

Configuration Quirks

  • Ancestry Column Type:

    • Default: string (255 chars). For deeper trees, consider text.
    • Migration example:
      $table->text('ancestry')->nullable();
      
  • Case Sensitivity:

    • Ancestry strings are case-sensitive. Normalize case if needed:
      $model->ancestry = strtolower($model->ancestry);
      
  • Empty Ancestry:

    • Root nodes have null ancestry. Ensure your queries handle this:
      $roots = Category::whereNull('ancestry')->get();
      

Performance Optimizations

  • Indexing: Add database indexes for faster queries:

    Schema::table('categories', function (Blueprint $table) {
        $table->index('ancestry');
    });
    
  • Materialized Path Alternative: For read-heavy workloads, consider storing materialized paths (e.g., left/right columns) alongside ancestry for faster queries.

  • Batch Operations: Use chunking for bulk hierarchy updates:

    Category::chunk(200, function ($categories) {
        foreach ($categories as $category) {
            $category->moveTo($newParent);
        }
    });
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
craftcms/url-validator
directorytree/privacy-filter-classifier
directorytree/privacy-filter
datacore/hub-sdk
develia/commons
cuci/prototurk-sdk
cuci/prototurk-sdk-symfony
develia/geo-bundle
dreamzy/livewire-charts
touchestate-sdk/php-sdk
22h/doctrine-garbage-collection-bundle
agtp/agtp-php
agtp/mod-php
splash/sonata-admin
splash/metadata
splash/openapi
splash/scopes
splash/toolkit
testo/output-teamcity
testo/bridge-symfony