Laravel Adjacency List

repository·main·Indexed 23 days ago

https://github.com/staudenmeir/laravel-adjacency-list

An Eloquent extension for Laravel that enables recursive relationships for trees and graphs using Common Table Expressions (CTEs). It provides traits for one-to-many trees (HasRecursiveRelationships) and many-to-many graphs (HasGraphRelationships), allowing for efficient hierarchical data traversal, cycle detection, and depth-based filtering across supported databases including MySQL 8.0+, MariaDB 10.2+, PostgreSQL 9.4+, SQLite 3.8.3+, SQL Server 2008+, and SingleStore 8.1+.

Tokens
4K
Snippets
10
Records
17
Agent score
82%

What's inside laravel-adjacency-list

  1. Setup Graphs (Many-to-Many) with HasGraphRelationships

    main

    To implement a graph structure where nodes can have multiple parents via a pivot table, use the HasGraphRelationships trait.

    1. Specify the pivot table name using getPivotTableName().
    2. Customize parent/child keys using getParentKeyName() and getChildKeyName() (defaults are parent_id and child_id).
    3. Customize the local key using getLocalKeyName().
    4. To access extra columns from the pivot table, override getPivotColumns().
    class Node extends Model
    {
        use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships;
    
        public function getPivotTableName(): string
        {
            return 'edges';
        }
    
        public function getParentKeyName(): string
        {
            return 'source_id';
        }
      
        public function getChildKeyName(): string
        {
            return 'target_id';
        }
    
        public function getPivotColumns(): array
        {
            return ['label', 'weight'];
        }
    }
    
    // Accessing pivot data
    $nodes = Node::find($id)->descendants;
    foreach ($nodes as $node) {
        dump($node->pivot->label, $node->pivot->weight);
    }
  2. Install Laravel Adjacency List via Composer

    main

    Install the package using Composer. For standard environments, use the standard requirement command. If you are using PowerShell on Windows (such as within VS Code), use the version with four carets to ensure compatibility.

    Standard:

    composer require staudenmeir/laravel-adjacency-list:"^1.0"

    PowerShell (Windows):

    composer require staudenmeir/laravel-adjacency-list:"^^^^1.0"
  3. Setup Trees (One-to-Many) with HasRecursiveRelationships

    main

    To implement a tree structure where each node has exactly one parent (e.g., categories, nested comments), use the HasRecursiveRelationships trait in your Eloquent model.

    By default, the trait expects a parent_id column and uses the model's primary key as the local key. You can customize these by overriding getParentKeyName() and getLocalKeyName().

    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('parent_id')->nullable();
    });
    
    class User extends Model
    {
        use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
    
        public function getParentKeyName()
        {
            return 'parent_id';
        }
    
        public function getLocalKeyName()
        {
            return 'id';
        }
    }
  4. Customize Tree Paths

    main

    Tree queries include a path column containing a dot-separated string of local keys. You can customize this behavior in your model:

    • getPathName(): Change the name of the path column.
    • getPathSeparator(): Change the separator (default is .).
    • getCustomPaths(): Define additional path columns based on other model attributes (e.g., slugs). You can also set 'reverse' => true for these custom paths.
    class User extends Model
    {
        public function getPathSeparator()
        {
            return '.';
        }
    
        public function getCustomPaths()
        {
            return [
                [
                    'name' => 'slug_path',
                    'column' => 'slug',
                    'separator' => '/',
                ],
            ];
        }
    }
    
    // Usage
    $descendants = User::find(1)->descendantsAndSelf;
    echo $descendants[1]->slug_path; // user-1/user-2
  5. Filter Trees by Position, Order, and Depth

    main

    The package provides several scopes to filter and order tree results:

    Position Filters:

    • hasChildren(): Models with children.
    • hasParent(): Models with a parent.
    • isLeaf() / doesntHaveChildren(): Models without children.
    • isRoot(): Models without a parent.

    Ordering:

    • breadthFirst(): Siblings before children.
    • depthFirst(): Children before siblings.

    Depth: Results include a depth column (relative to the query parent).

    • whereDepth($operator, $value): Filter by relative depth (e.g., whereDepth('<', 3)).
    • withMaxDepth($maxDepth, $callback): Improves performance by only building the requested section of the tree up to $maxDepth.
    // Position
    $leaves = User::isLeaf()->get();
    $roots = User::isRoot()->get();
    
    // Order
    $tree = User::tree()->breadthFirst()->get();
    
    // Depth
    $descendants = User::find($id)->descendants()->whereDepth('<', 3)->get();
    
    // Performance optimized depth query
    $descendants = User::withMaxDepth(3, function () use ($id) {
        return User::find($id)->descendants;
    });
  6. Enable Cycle Detection in Trees

    main

    If your tree data might contain cycles (a node being its own ancestor), enable cycle detection to prevent infinite loops.

    By overriding enableCycleDetection() to return true, the query results will include an is_cycle column indicating if a node is part of a cycle. You can also use includeCycleStart() to identify the first duplicate node in the cycle.

    class User extends Model
    {
        public function enableCycleDetection(): bool
        {
            return true;
        }
    
        public function includeCycleStart(): bool
        {
            return true;
        }
    }
    
    $users = User::find($id)->descendants;
    foreach ($users as $user) {
        dump($user->is_cycle);
    }
  7. Query Trees and Subtrees

    main

    Use the tree() query scope to retrieve all models starting from the root(s).

    • tree(): Gets the entire tree.
    • treeOf($constraint, $maxDepth): Queries trees starting from roots that match a specific $constraint (a closure). You can also specify a $maxDepth.
    • loadTreeRelationships(): Chaperones tree relationships (like ancestors and parent) to reduce N+1 queries when working with a tree collection.
    • toTree(): Converts a flat collection of tree models into a nested structure by recursively setting children relationships.
    // Get the whole tree
    $tree = User::tree()->get();
    
    // Get tree with custom root constraints and depth
    $constraint = function ($query) {
        $query->whereNull('parent_id')->where('list_id', 1);
    };
    $tree = User::treeOf($constraint, 3)->get();
    
    // Optimize N+1 and nest results
    $users = User::tree(3)->get();
    $tree = $users->loadTreeRelationships()->toTree();
  8. Use recursive relationships in Trees

    main

    The HasRecursiveRelationships trait provides several relationships for traversing tree structures:

    • ancestors(): Recursive parents.
    • ancestorsAndSelf(): Recursive parents and the model itself.
    • bloodline(): Ancestors, descendants, and itself.
    • children(): Direct children.
    • childrenAndSelf(): Direct children and itself.
    • descendants(): Recursive children.
    • descendantsAndSelf(): Recursive children and itself.
    • parent(): Direct parent.
    • parentAndSelf(): Direct parent and itself.
    • rootAncestor(): Topmost parent.
    • rootAncestorOrSelf(): Topmost parent or itself.
    • siblings(): Other children of the same parent.
    • siblingsAndSelf(): All children of the same parent.

    You can use these as standard Eloquent relationships for eager loading, filtering, or counting.

    $ancestors = User::find($id)->ancestors;
    
    $users = User::with('descendants')->get();
    
    $users = User::whereHas('siblings', function ($query) {
        $query->where('name', 'John');
    })->get();
    
    $total = User::find($id)->descendants()->count();
    
    User::find($id)->descendants()->update(['active' => false]);
    
    User::find($id)->siblings()->delete();
  9. Use recursive relationships in Graphs

    main

    The HasGraphRelationships trait provides relationships for traversing directed graphs:

    • ancestors(): Recursive parents.
    • ancestorsAndSelf(): Recursive parents and itself.
    • children(): Direct children.
    • childrenAndSelf(): Direct children and itself.
    • descendants(): Recursive children.
    • descendantsAndSelf(): Recursive children and itself.
    • parents(): Direct parents.
    • parentsAndSelf(): Direct parents and itself.

    Additionally, you can use the subgraph($constraint, $maxDepth) scope to retrieve a subgraph based on a custom query constraint.

    $ancestors = Node::find($id)->ancestors;
    
    $nodes = Node::with('descendants')->get();
    
    $nodes = Node::has('children')->get();
    
    // Subgraph query
    $constraint = function ($query) use ($ids) {
        $query->whereIn('id', $ids);
    };
    $subgraph = Node::subgraph($constraint, 3)->get();
  10. Define Custom Recursive Relationships (HasMany/BelongsToMany/MorphToMany)

    main

    You can define custom relationships that traverse the tree/graph and then access related models. For example, if a User has many Posts, you can define a relationship to get all posts belonging to a user AND all their descendants.

    • hasManyOfDescendantsAndSelf(RelatedClass): Gets related models for the node and all its descendants.
    • hasManyOfDescendants(RelatedClass): Gets related models for descendants only.
    • belongsToManyOfDescendantsAndSelf(RelatedClass, pivotName): For many-to-many relationships.
    • morphToManyOfDescendantsAndSelf(RelatedClass, morphName): For polymorphic many-to-many relationships.
    • morphedByManyOfDescendantsAndSelf(RelatedClass, morphName): For the inverse polymorphic relationship.
    // Example: User -> Posts (HasMany)
    class User extends Model
    {
        public function recursivePosts()
        {
            return $this->hasManyOfDescendantsAndSelf(Post::class);
        }
    }
    
    // Example: User -> Roles (BelongsToMany)
    class User extends Model
    {
        public function recursiveRoles()
        {
            return $this->belongsToManyOfDescendantsAndSelf(Role::class);
        }
    }
    
    // Usage
    $recursivePosts = User::find($id)->recursivePosts;
    $users = User::withCount('recursivePosts')->get();
  11. Configure IDE Helper model hooks

    main

    The package automatically registers a RecursiveRelationsHook with the laravel-ide-helper package to ensure that recursive adjacency list relationships are correctly documented in your IDE.

    If you are using barryvdh/laravel-ide-helper, this package injects Staudenmeir\LaravelAdjacencyList\IdeHelper\RecursiveRelationsHook into the ide-helper.model_hooks configuration array. You can view or modify your existing model hooks in your config/ide-helper.php file.

  12. Check database compatibility for Laravel Adjacency List

    main

    The package uses Common Table Expressions (CTEs) to provide recursive relationships. Ensure your database engine meets the following minimum version requirements:

    • MySQL: 8.0+
    • MariaDB: 10.2+
    • PostgreSQL: 9.4+
    • SQLite: 3.8.3+
    • SQL Server: 2008+
    • SingleStore: 8.1+ (Note: SingleStore only supports trees (one-to-many), not graphs).