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

Laravel Adjacency List Laravel Package

staudenmeir/laravel-adjacency-list

Laravel Eloquent extension for recursive tree and graph relationships using SQL common table expressions. Traverse ancestors, descendants, and paths in adjacency-list data across MySQL, Postgres, SQLite, SQL Server, and more; supports one-to-many trees and many-to-many graphs.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require staudenmeir/laravel-adjacency-list
    

    Ensure your database driver (MySQL 8.0+, PostgreSQL 9.4+, etc.) supports CTEs.

  2. Model Integration: Add the trait to your model:

    use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
    
    class Category extends Model
    {
        use HasRecursiveRelationships;
    }
    
  3. First Query: Fetch a node and its descendants:

    $category = Category::find(1);
    $descendants = $category->descendants; // Recursive children
    
  4. Tree Query: Fetch the entire tree from root nodes:

    $tree = Category::tree()->get();
    

Implementation Patterns

Core Workflows

  1. Hierarchical Data Fetching:

    • Ancestors: $model->ancestors (recursive parents).
    • Descendants: $model->descendants (recursive children).
    • Bloodline: $model->bloodline (ancestors + descendants + self).
  2. Tree Construction:

    • Flat to Nested: Convert a flat collection to a nested tree:
      $flatTree = Category::tree()->get();
      $nestedTree = $flatTree->toTree();
      
    • Chaperoned Loading: Load parent/ancestor relationships in bulk:
      $tree = Category::tree()->get()->loadTreeRelationships();
      
  3. Filtering and Ordering:

    • Depth Constraints: Limit results by depth:
      $category->descendants()->whereDepth(2)->get();
      
    • Breadth/Depth-First: Order traversal:
      $category->descendants()->breadthFirst()->get();
      
  4. Custom Paths:

    • Add custom path columns (e.g., slug paths):
      class Category extends Model
      {
          public function getCustomPaths()
          {
              return [
                  [
                      'name' => 'slug_path',
                      'column' => 'slug',
                      'separator' => '/',
                  ],
              ];
          }
      }
      
  5. Cycle Detection: Enable for cyclic graphs:

    class Category extends Model
    {
        public function enableCycleDetection(): bool { return true; }
    }
    

Integration Tips

  1. Eager Loading: Use with() to avoid N+1 queries:

    $categories = Category::with('descendants')->get();
    
  2. Query Scopes: Filter trees dynamically:

    Category::treeOf(function ($query) {
        $query->where('is_published', true);
    })->get();
    
  3. Custom Relationships: Extend existing relationships (e.g., for polymorphic models):

    class Category extends Model
    {
        public function hasManyOfDescendants(Post $post)
        {
            return $this->hasManyOfDescendantsAndSelf(Post::class);
        }
    }
    
  4. Depth Optimization: Use withMaxDepth() for large trees:

    Category::withMaxDepth(3, function () {
        return Category::find(1)->descendants;
    })->get();
    
  5. Path Manipulation: Reverse or customize paths for URLs:

    $category->slug_path; // "parent/child"
    $category->reverse_slug_path; // "child/parent"
    

Gotchas and Tips

Pitfalls

  1. Database Compatibility:

    • SQLite < 3.8.3 or older MySQL versions won’t work (CTE support required).
    • Test queries in your target database before production use.
  2. Cycle Detection Overhead: Enabling enableCycleDetection() adds query complexity. Use only if cycles exist.

  3. Depth Column Conflicts: If your table already has a depth column, override getDepthName():

    public function getDepthName() { return 'tree_depth'; }
    
  4. Performance with Large Trees:

    • Avoid descendants() on deep trees without withMaxDepth().
    • Use breadthFirst() for wide trees to limit memory usage.
  5. Custom Paths and NULL Values: Custom paths may fail if the referenced column (slug) is NULL. Add a default:

    return [
        'name' => 'slug_path',
        'column' => 'slug',
        'separator' => '/',
        'default' => 'root',
    ];
    

Debugging

  1. Query Inspection: Use Laravel’s query logging or toSql() to verify CTEs:

    $query = Category::find(1)->descendants()->toSql();
    
  2. Cycle Detection: Check for is_cycle in results when enabled:

    foreach ($model->descendants as $item) {
        if ($item->is_cycle) {
            // Handle cycle
        }
    }
    
  3. Path Separator Issues: Ensure separators (e.g., /) don’t conflict with data (e.g., URLs with / in slugs).


Extension Points

  1. Custom Scopes: Extend the trait to add domain-specific scopes:

    class Category extends Model
    {
        public function scopeActiveTree($query)
        {
            return $query->treeOf(function ($q) {
                $q->where('is_active', true);
            });
        }
    }
    
  2. Dynamic Relationships: Use closures for dynamic relationships:

    public function dynamicDescendants($model)
    {
        return $this->hasManyOfDescendants($model);
    }
    
  3. Path Formatting: Override getPathSeparator() or add custom path logic:

    public function getCustomPaths()
    {
        return [
            [
                'name' => 'url_path',
                'column' => 'slug',
                'separator' => '-',
                'format' => fn($path) => strtolower($path),
            ],
        ];
    }
    
  4. Hybrid Trees/Graphs: Combine with many-to-many relationships for complex graphs:

    // In a pivot table
    public function scopeGraphTree($query)
    {
        return $query->with(['parents' => function ($q) {
            $q->with('children');
        }]);
    }
    

Pro Tips

  1. Bulk Operations: Use update() or delete() on relationships:

    $category->descendants()->update(['visibility' => 'hidden']);
    
  2. Depth-Based Logic: Filter by depth in queries:

    $category->descendants()->whereDepth('>', 2)->get();
    
  3. Tree Validation: Validate tree integrity with:

    $category->isRoot(); // Check if no parent
    $category->isLeaf(); // Check for children
    
  4. Caching: Cache tree structures for read-heavy apps:

    Cache::remember('tree', 60, function () {
        return Category::tree()->get()->toTree();
    });
    
  5. Testing: Use factories to build test trees:

    $root = Category::factory()->create();
    $child = Category::factory()->create(['parent_id' => $root->id]);
    
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.
calmfox/watch-sylius
damienfern/grpc-symfony-bundle
atoolo/index-bundle
atoolo/genai-bundle
coprotoai/laravel-ticket
davidjln/llm-carbon-bundle
cryonighter/valid-request-bundle
coolms/taxonomy-bundle
coolms/field-bundle
articulate-orm/symfony
aaix/laravel-tall-architect
ephoto/akeneo-connector
emmanuelballery/eb-plantumlbundle
emielburgman/symfony-visitor-beacon
emielburgman/symfony-visit-storage
emielburgman/symfony-security-headers
emielburgman/symfony-log-viewer
emarref/xdebug-bundle
emarref/pubnub-bundle
elriseio/finance-money-bundle