PHPackages                             staudenmeir/laravel-adjacency-list - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. staudenmeir/laravel-adjacency-list

ActiveLibrary[Database &amp; ORM](/categories/database)

staudenmeir/laravel-adjacency-list
==================================

Recursive Laravel Eloquent relationships with CTEs

v1.26(2mo ago)1.5k4.7M—3.7%112[6 issues](https://github.com/staudenmeir/laravel-adjacency-list/issues)20MITPHPPHP ^8.3CI passing

Since Apr 25Pushed 2mo ago21 watchersCompare

[ Source](https://github.com/staudenmeir/laravel-adjacency-list)[ Packagist](https://packagist.org/packages/staudenmeir/laravel-adjacency-list)[ Fund](https://paypal.me/JonasStaudenmeir)[ RSS](/packages/staudenmeir-laravel-adjacency-list/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (21)Versions (105)Used By (20)

Laravel Adjacency List
======================

[](#laravel-adjacency-list)

[![CI](https://github.com/staudenmeir/laravel-adjacency-list/actions/workflows/ci.yml/badge.svg)](https://github.com/staudenmeir/laravel-adjacency-list/actions/workflows/ci.yml?query=branch%3Amain)[![Code Coverage](https://camo.githubusercontent.com/d3b902b575d995283f86eddf445ba74d57cc737e6c218b3131afbbf7513405b5/68747470733a2f2f636f6465636f762e696f2f67682f7374617564656e6d6569722f6c61726176656c2d61646a6163656e63792d6c6973742f67726170682f62616467652e7376673f746f6b656e3d56685a336f4268315945)](https://codecov.io/gh/staudenmeir/laravel-adjacency-list)[![PHPStan](https://camo.githubusercontent.com/83dd3d35cebed0eab9ee97ff1a5849c1344cda6a8ee9cac2cda20f5aa55b67bd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d627269676874677265656e2e7376673f7374796c653d666c6174)](https://github.com/staudenmeir/laravel-adjacency-list/actions/workflows/static-analysis.yml?query=branch%3Amain)[![Latest Stable Version](https://camo.githubusercontent.com/f4d30ad92678faa9fdf539881f45b63b5af48a9085b38bb4859f884bb9a261ac/68747470733a2f2f706f7365722e707567782e6f72672f7374617564656e6d6569722f6c61726176656c2d61646a6163656e63792d6c6973742f762f737461626c65)](https://packagist.org/packages/staudenmeir/laravel-adjacency-list)[![Total Downloads](https://camo.githubusercontent.com/6bd805dbe1d9754509e88afb9f610dbfa263417afcd5977518160cde2edf2ffc/68747470733a2f2f706f7365722e707567782e6f72672f7374617564656e6d6569722f6c61726176656c2d61646a6163656e63792d6c6973742f646f776e6c6f616473)](https://packagist.org/packages/staudenmeir/laravel-adjacency-list/stats)[![License](https://camo.githubusercontent.com/5cfa475b674e2db581bf346d411e5d1105a767786258524cd8deb9b8444933e9/68747470733a2f2f706f7365722e707567782e6f72672f7374617564656e6d6569722f6c61726176656c2d61646a6163656e63792d6c6973742f6c6963656e7365)](https://github.com/staudenmeir/laravel-adjacency-list/blob/main/LICENSE)

This Laravel Eloquent extension provides recursive relationships for [trees](#trees-one-parent-per-node-one-to-many) and [graphs](#graphs-multiple-parents-per-node-many-to-many) using common table expressions (CTE).

Compatibility
-------------

[](#compatibility)

- MySQL 8.0+
- MariaDB 10.2+
- PostgreSQL 9.4+
- SQLite 3.8.3+
- SQL Server 2008+
- SingleStore 8.1+ (only [trees](#trees-one-parent-per-node-one-to-many))

Installation
------------

[](#installation)

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

```

Use this command if you are in PowerShell on Windows (e.g. in VS Code):

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

```

Versions
--------

[](#versions)

LaravelPackage13.x1.2612.x1.2411.x1.2110.x1.139.x1.128.x1.97.x1.56.x1.35.81.15.5–5.71.0Usage
-----

[](#usage)

The package offers recursive relationships for traversing two types of data structures:

- [Trees: One Parent per Node (One-to-Many)](#trees-one-parent-per-node-one-to-many)
- [Graphs: Multiple Parents per Node (Many-to-Many)](#graphs-multiple-parents-per-node-many-to-many)

### Trees: One Parent per Node (One-to-Many)

[](#trees-one-parent-per-node-one-to-many)

Use the package to traverse a tree structure with one parent per node. Use cases might be recursive categories, a page hierarchy or nested comments.

Supports Laravel 5.5+.

- [Getting Started](#getting-started)
- [Included Relationships](#included-relationships)
- [Trees](#trees)
- [Filters](#filters)
- [Order](#order)
- [Depth](#depth)
- [Path](#path)
- [Custom Paths](#custom-paths)
- [Nested Results](#nested-results)
- [Initial &amp; Recursive Query Constraints](#initial--recursive-query-constraints)
- [Cycle Detection](#cycle-detection)
- [Additional Methods](#additional-methods)
- [Custom Relationships](#custom-relationships)
- [Deep Relationship Concatenation](#deep-relationship-concatenation)
- [Known Issues](#known-issues)

#### Getting Started

[](#getting-started)

Consider the following table schema for hierarchical data in trees:

```
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('parent_id')->nullable();
});
```

Use the `HasRecursiveRelationships` trait in your model to work with recursive relationships:

```
class User extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
}
```

By default, the trait expects a parent key named `parent_id`. You can customize it by overriding `getParentKeyName()`:

```
class User extends Model
{
    public function getParentKeyName()
    {
        return 'parent_id';
    }
}
```

By default, the trait uses the model's primary key as the local key. You can customize it by overriding `getLocalKeyName()`:

```
class User extends Model
{
    public function getLocalKeyName()
    {
        return 'id';
    }
}
```

#### Included Relationships

[](#included-relationships)

The trait provides various relationships:

- `ancestors()`: The model's recursive parents.
- `ancestorsAndSelf()`: The model's recursive parents and itself.
- `bloodline()`: The model's ancestors, descendants and itself.
- `children()`: The model's direct children.
- `childrenAndSelf()`: The model's direct children and itself.
- `descendants()`: The model's recursive children.
- `descendantsAndSelf()`: The model's recursive children and itself.
- `parent()`: The model's direct parent.
- `parentAndSelf()`: The model's direct parent and itself.
- `rootAncestor()`: The model's topmost parent.
- `rootAncestorOrSelf()`: The model's topmost parent or itself.
- `siblings()`: The parent's other children.
- `siblingsAndSelf()`: All the parent's children.

```
$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();
```

#### Trees

[](#trees)

The trait provides the `tree()` query scope to get all models, beginning at the root(s):

```
$tree = User::tree()->get();
```

`treeOf()` allows you to query trees with custom constraints for the root model(s). Consider a table with multiple separate lists:

```
$constraint = function ($query) {
    $query->whereNull('parent_id')->where('list_id', 1);
};

$tree = User::treeOf($constraint)->get();
```

You can also pass a maximum depth:

```
$tree = User::tree(3)->get();

$tree = User::treeOf($constraint, 3)->get();
```

You can also chaperone tree relationships to load `ancestors` and `parent` relationships already present in the tree to (potentially) reduce N+1 queries:

```
$users = User::tree(3)->get();

$users->loadTreeRelationships();
```

Or with `toTree()`:

```
$users = User::tree(1)->get();

$tree = $users->loadTreeRelationships()->toTree();
```

#### Filters

[](#filters)

The trait provides query scopes to filter models by their position in the tree:

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

```
$noLeaves = User::hasChildren()->get();

$noRoots = User::hasParent()->get();

$leaves = User::isLeaf()->get();
$leaves = User::doesntHaveChildren()->get();

$roots = User::isRoot()->get();
```

#### Order

[](#order)

The trait provides query scopes to order models breadth-first or depth-first:

- `breadthFirst()`: Get siblings before children.
- `depthFirst()`: Get children before siblings.

```
$tree = User::tree()->breadthFirst()->get();

$descendants = User::find($id)->descendants()->depthFirst()->get();
```

#### Depth

[](#depth)

The results of ancestor, bloodline, descendant and tree queries include an additional `depth` column.

It contains the model's depth *relative* to the query's parent. The depth is positive for descendants and negative for ancestors:

```
$descendantsAndSelf = User::find($id)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->depth; // 0
echo $descendantsAndSelf[1]->depth; // 1
echo $descendantsAndSelf[2]->depth; // 2
```

Change the column name by overriding `getDepthName()` if your table already contains a `depth` column:

```
class User extends Model
{
    public function getDepthName()
    {
        return 'depth';
    }
}
```

##### Depth Constraints

[](#depth-constraints)

You can use the `whereDepth()` query scope to filter models by their relative depth:

```
$descendants = User::find($id)->descendants()->whereDepth(2)->get();

$descendants = User::find($id)->descendants()->whereDepth('ancestors;
});
```

#### Path

[](#path)

The results of ancestor, bloodline, descendant and tree queries include an additional `path` column.

It contains the dot-separated path of local keys from the query's parent to the model:

```
$descendantsAndSelf = User::find(1)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->path; // 1
echo $descendantsAndSelf[1]->path; // 1.2
echo $descendantsAndSelf[2]->path; // 1.2.3
```

Change the column name by overriding `getPathName()` if your table already contains a `path` column:

```
class User extends Model
{
    public function getPathName()
    {
        return 'path';
    }
```

You can also customize the path separator by overriding `getPathSeparator()`:

```
class User extends Model
{
    public function getPathSeparator()
    {
        return '.';
    }
}
```

#### Custom Paths

[](#custom-paths)

You can add custom path columns to the query results:

```
class User extends Model
{
    public function getCustomPaths()
    {
        return [
            [
                'name' => 'slug_path',
                'column' => 'slug',
                'separator' => '/',
            ],
        ];
    }
}

$descendantsAndSelf = User::find(1)->descendantsAndSelf;

echo $descendantsAndSelf[0]->slug_path; // user-1
echo $descendantsAndSelf[1]->slug_path; // user-1/user-2
echo $descendantsAndSelf[2]->slug_path; // user-1/user-2/user-3
```

You can also reverse custom paths:

```
class User extends Model
{
    public function getCustomPaths()
    {
        return [
            [
                'name' => 'reverse_slug_path',
                'column' => 'slug',
                'separator' => '/',
                'reverse' => true,
            ],
        ];
    }
}
```

#### Nested Results

[](#nested-results)

Use the `toTree()` method on a result collection to generate a nested tree:

```
$users = User::tree()->get();

$tree = $users->toTree();
```

This recursively sets `children` relationships:

```
[
  {
    "id": 1,
    "children": [
      {
        "id": 2,
        "children": [
          {
            "id": 3,
            "children": []
          }
        ]
      },
      {
        "id": 4,
        "children": [
          {
            "id": 5,
            "children": []
          }
        ]
      }
    ]
  }
]
```

#### Initial &amp; Recursive Query Constraints

[](#initial--recursive-query-constraints)

You can add custom constraints to the CTE's initial and recursive query. Consider a query where you want to traverse a tree while skipping inactive users and their descendants:

```
$tree = User::withQueryConstraint(function (Builder $query) {
   $query->where('users.active', true);
}, function () {
   return User::tree()->get();
});
```

You can also add a custom constraint to only the initial or recursive query using `withInitialQueryConstraint()`/ `withRecursiveQueryConstraint()`.

#### Cycle Detection

[](#cycle-detection)

If your tree contains cycles, you need to enable cycle detection to prevent infinite loops:

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

You can also retrieve the start of a cycle, i.e., the first duplicate node. With this option, the query results include an `is_cycle` column that indicates whether the node is part of a 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);
}
```

#### Additional Methods

[](#additional-methods)

The trait also provides methods to check relationships between models:

- `isChildOf(Model $model)`: Checks if the current model is a child of the given model.
- `isParentOf(Model $model)`: Checks if the current model is a parent of the given model.
- `getDepthRelatedTo(Model $model)`: Returns the depth of the current model related to the given model.

```
$rootUser = User::create(['parent_id' => null]);
$firstLevelUser = User::create(['parent_id' => $rootUser->id]);
$secondLevelUser = User::create(['parent_id' => $firstLevelUser->id]);

$isChildOf = $secondLevelUser->isChildOf($firstLevelUser); // Output: true
$isParentOf = $rootUser->isParentOf($firstLevelUser); // Output: true
$depthRelatedTo = $secondLevelUser->getDepthRelatedTo($rootUser); // Output: 2
```

#### Custom Relationships

[](#custom-relationships)

You can also define custom relationships to retrieve related models recursively.

- [HasManyOfDescendants](#hasmanyofdescendants)
- [BelongsToManyOfDescendants](#belongstomanyofdescendants)
- [MorphToManyOfDescendants](#morphtomanyofdescendants)
- [MorphedByManyOfDescendants](#morphedbymanyofdescendants)
- [Intermediate Scopes](#intermediate-scopes)
- [Usage outside of Laravel](#usage-outside-of-laravel)

##### HasManyOfDescendants

[](#hasmanyofdescendants)

Consider a `HasMany` relationship between `User` and `Post`:

```
class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
```

Define a `HasManyOfDescendants` relationship to get all posts of a user and its descendants:

```
class User extends Model
{
    public function recursivePosts()
    {
        return $this->hasManyOfDescendantsAndSelf(Post::class);
    }
}

$recursivePosts = User::find($id)->recursivePosts;

$users = User::withCount('recursivePosts')->get();
```

Use `hasManyOfDescendants()` to only get the descendants' posts:

```
class User extends Model
{
    public function descendantPosts()
    {
        return $this->hasManyOfDescendants(Post::class);
    }
}
```

##### BelongsToManyOfDescendants

[](#belongstomanyofdescendants)

Consider a `BelongsToMany` relationship between `User` and `Role`:

```
class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}
```

Define a `BelongsToManyOfDescendants` relationship to get all roles of a user and its descendants:

```
class User extends Model
{
    public function recursiveRoles()
    {
        return $this->belongsToManyOfDescendantsAndSelf(Role::class);
    }
}

$recursiveRoles = User::find($id)->recursiveRoles;

$users = User::withCount('recursiveRoles')->get();
```

Use `belongsToManyOfDescendants()` to only get the descendants' roles:

```
class User extends Model
{
    public function descendantRoles()
    {
        return $this->belongsToManyOfDescendants(Role::class);
    }
}
```

##### MorphToManyOfDescendants

[](#morphtomanyofdescendants)

Consider a `MorphToMany` relationship between `User` and `Tag`:

```
class User extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}
```

Define a `MorphToManyOfDescendants` relationship to get all tags of a user and its descendants:

```
class User extends Model
{
    public function recursiveTags()
    {
        return $this->morphToManyOfDescendantsAndSelf(Tag::class, 'taggable');
    }
}

$recursiveTags = User::find($id)->recursiveTags;

$users = User::withCount('recursiveTags')->get();
```

Use `morphToManyOfDescendants()` to only get the descendants' tags:

```
class User extends Model
{
    public function descendantTags()
    {
        return $this->morphToManyOfDescendants(Tag::class, 'taggable');
    }
}
```

##### MorphedByManyOfDescendants

[](#morphedbymanyofdescendants)

Consider a `MorphedByMany` relationship between `Category` and `Post`:

```
class Category extends Model
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'categorizable');
    }
}
```

Define a `MorphedByManyOfDescendants` relationship to get all posts of a category and its descendants:

```
class Category extends Model
{
    public function recursivePosts()
    {
        return $this->morphedByManyOfDescendantsAndSelf(Post::class, 'categorizable');
    }
}

$recursivePosts = Category::find($id)->recursivePosts;

$categories = Category::withCount('recursivePosts')->get();
```

Use `morphedByManyOfDescendants()` to only get the descendants' posts:

```
class Category extends Model
{
    public function descendantPosts()
    {
        return $this->morphedByManyOfDescendants(Post::class, 'categorizable');
    }
}
```

##### Intermediate Scopes

[](#intermediate-scopes)

You can adjust the descendants query (e.g. child users) by adding or removing intermediate scopes:

```
User::find($id)->recursivePosts()->withTrashedDescendants()->get();

User::find($id)->recursivePosts()->withIntermediateScope('active', new ActiveScope())->get();

User::find($id)->recursivePosts()->withIntermediateScope(
    'depth',
    function ($query) {
        $query->whereDepth('hasManyDeepFromRelations(
            $this->descendants(),
            (new static)->posts()
        );
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

$descendantPosts = User::find($id)->descendantPosts;
```

At the moment, recursive relationships can only be at the beginning of deep relationships:

- Supported: `User` → descendants → `User` → has many → `Post`
- Not supported: `Post` → belongs to → `User` → descendants → `User`

#### Known Issues

[](#known-issues)

MariaDB [doesn't yet support](https://jira.mariadb.org/browse/MDEV-19077) correlated CTEs in subqueries. This affects queries like `User::whereHas('descendants')` or `User::withCount('descendants')`.

### Graphs: Multiple Parents per Node (Many-to-Many)

[](#graphs-multiple-parents-per-node-many-to-many)

You can also use the package to traverse graphs with multiple parents per node that are defined in a pivot table. Use cases might be a bill of materials (BOM) or a family tree.

Supports Laravel 9+.

- [Getting Started](#graphs-getting-started)
- [Included Relationships](#graphs-included-relationships)
- [Pivot Columns](#graphs-pivot-columns)
- [Cycle Detection](#graphs-cycle-detection)
- [Subgraphs](#graphs-subgraphs)
- [Order](#graphs-order)
- [Depth](#graphs-depth)
- [Path](#graphs-path)
- [Custom Paths](#graphs-custom-paths)
- [Nested Results](#graphs-nested-results)
- [Initial &amp; Recursive Query Constraints](#graphs-initial--recursive-query-constraints)
- [Deep Relationship Concatenation](#graphs-deep-relationship-concatenation)
- [Known Issues](#graphs-known-issues)

#### Getting Started

[](#getting-started-1)

Consider the following table schema for storing directed graphs as nodes and edges:

```
Schema::create('nodes', function (Blueprint $table) {
    $table->id();
});

Schema::create('edges', function (Blueprint $table) {
    $table->unsignedBigInteger('source_id');
    $table->unsignedBigInteger('target_id');
    $table->string('label');
    $table->unsignedBigInteger('weight');
});
```

Use the `HasGraphRelationships` trait in your model to work with graph relationships and specify the name of the pivot table:

```
class Node extends Model
{
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships;

    public function getPivotTableName(): string
    {
        return 'edges';
    }
}
```

By default, the trait expects a parent key named `parent_id` and child key named `child_id` in the pivot table. You can customize them by overriding `getParentKeyName()` and `getChildKeyName()`:

```
class Node extends Model
{
    public function getParentKeyName(): string
    {
        return 'source_id';
    }

    public function getChildKeyName(): string
    {
        return 'target_id';
    }
}
```

By default, the trait uses the model's primary key as the local key. You can customize it by overriding `getLocalKeyName()`:

```
class Node extends Model
{
    public function getLocalKeyName(): string
    {
        return 'id';
    }
}
```

#### Included Relationships

[](#included-relationships-1)

The trait provides various relationships:

- `ancestors()`: The node's recursive parents.
- `ancestorsAndSelf()`: The node's recursive parents and itself.
- `children()`: The node's direct children.
- `childrenAndSelf()`: The node's direct children and itself.
- `descendants()`: The node's recursive children.
- `descendantsAndSelf()`: The node's recursive children and itself.
- `parents()`: The node's direct parents.
- `parentsAndSelf()`: The node's direct parents and itself.

```
$ancestors = Node::find($id)->ancestors;

$nodes = Node::with('descendants')->get();

$nodes = Node::has('children')->get();

$total = Node::find($id)->descendants()->count();

Node::find($id)->descendants()->update(['active' => false]);

Node::find($id)->parents()->delete();
```

#### Pivot Columns

[](#pivot-columns)

Similar to `BelongsToMany` relationships, you can retrieve additional columns from the pivot table besides the parent and child key:

```
class Node extends Model
{
    public function getPivotColumns(): array
    {
        return ['label', 'weight'];
    }
}

$nodes = Node::find($id)->descendants;

foreach ($nodes as $node) {
    dump(
        $node->pivot->label,
        $node->pivot->weight
    );
}
```

#### Cycle Detection

[](#cycle-detection-1)

If your graph contains cycles, you need to enable cycle detection to prevent infinite loops:

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

You can also retrieve the start of a cycle, i.e., the first duplicate node. With this option, the query results include an `is_cycle` column that indicates whether the node is part of a cycle:

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

    public function includeCycleStart(): bool
    {
        return true;
    }
}

$nodes = Node::find($id)->descendants;

foreach ($nodes as $node) {
    dump($node->is_cycle);
}
```

#### Subgraphs

[](#subgraphs)

The trait provides the `subgraph()` query scope to get the subgraph of a custom constraint:

```
$constraint = function ($query) {
    $query->whereIn('id', $ids);
};

$subgraph = Node::subgraph($constraint)->get();
```

You can pass a maximum depth as the second argument:

```
$subgraph = Node::subgraph($constraint, 3)->get();
```

#### Order

[](#order-1)

The trait provides query scopes to order nodes breadth-first or depth-first:

- `breadthFirst()`: Get siblings before children.
- `depthFirst()`: Get children before siblings.

```
$descendants = Node::find($id)->descendants()->breadthFirst()->get();

$descendants = Node::find($id)->descendants()->depthFirst()->get();
```

#### Depth

[](#depth-1)

The results of ancestor, descendant and subgraph queries include an additional `depth` column.

It contains the node's depth *relative* to the query's parent. The depth is positive for descendants and negative for ancestors:

```
$descendantsAndSelf = Node::find($id)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->depth; // 0
echo $descendantsAndSelf[1]->depth; // 1
echo $descendantsAndSelf[2]->depth; // 2
```

Change the column name by overriding `getDepthName()` if your table already contains a `depth` column:

```
class Node extends Model
{
    public function getDepthName(): string
    {
        return 'depth';
    }
}
```

##### Depth Constraints

[](#depth-constraints-1)

You can use the `whereDepth()` query scope to filter nodes by their relative depth:

```
$descendants = Node::find($id)->descendants()->whereDepth(2)->get();

$descendants = Node::find($id)->descendants()->whereDepth('ancestors;
});
```

#### Path

[](#path-1)

The results of ancestor, descendant and subgraph queries include an additional `path` column.

It contains the dot-separated path of local keys from the query's parent to the node:

```
$descendantsAndSelf = Node::find(1)->descendantsAndSelf()->depthFirst()->get();

echo $descendantsAndSelf[0]->path; // 1
echo $descendantsAndSelf[1]->path; // 1.2
echo $descendantsAndSelf[2]->path; // 1.2.3
```

Change the column name by overriding `getPathName()` if your table already contains a `path` column:

```
class Node extends Model
{
    public function getPathName(): string
    {
        return 'path';
    }
}
```

You can also customize the path separator by overriding `getPathSeparator()`:

```
class Node extends Model
{
    public function getPathSeparator(): string
    {
        return '.';
    }
}
```

#### Custom Paths

[](#custom-paths-1)

You can add custom path columns to the query results:

```
class Node extends Model
{
    public function getCustomPaths(): array
    {
        return [
            [
                'name' => 'slug_path',
                'column' => 'slug',
                'separator' => '/',
            ],
        ];
    }
}

$descendantsAndSelf = Node::find(1)->descendantsAndSelf;

echo $descendantsAndSelf[0]->slug_path; // node-1
echo $descendantsAndSelf[1]->slug_path; // node-1/node-2
echo $descendantsAndSelf[2]->slug_path; // node-1/node-2/node-3
```

You can also reverse custom paths:

```
class Node extends Model
{
    public function getCustomPaths(): array
    {
        return [
            [
                'name' => 'reverse_slug_path',
                'column' => 'slug',
                'separator' => '/',
                'reverse' => true,
            ],
        ];
    }
}
```

#### Nested Results

[](#nested-results-1)

Use the `toTree()` method on a result collection to generate a nested tree:

```
$nodes = Node::find($id)->descendants;

$tree = $nodes->toTree();
```

This recursively sets `children` relationships:

```
[
  {
    "id": 1,
    "children": [
      {
        "id": 2,
        "children": [
          {
            "id": 3,
            "children": []
          }
        ]
      },
      {
        "id": 4,
        "children": [
          {
            "id": 5,
            "children": []
          }
        ]
      }
    ]
  }
]
```

#### Initial &amp; Recursive Query Constraints

[](#initial--recursive-query-constraints-1)

You can add custom constraints to the CTE's initial and recursive query. Consider a query where you want to traverse a node's descendants while skipping inactive nodes and their descendants:

```
$descendants = Node::withQueryConstraint(function (Builder $query) {
   $query->where('nodes.active', true);
}, function () {
   return Node::find($id)->descendants;
});
```

You can also add a custom constraint to only the initial or recursive query using `withInitialQueryConstraint()`/ `withRecursiveQueryConstraint()`.

#### Deep Relationship Concatenation

[](#deep-relationship-concatenation-1)

You can include recursive relationships into deep relationships by concatenating them with other relationships using [staudenmeir/eloquent-has-many-deep](https://github.com/staudenmeir/eloquent-has-many-deep) (Laravel 9+).

Consider a `HasMany` relationship between `Node` and `Post` and building a deep relationship to get all posts of a node's descendants:

`Node` → descendants → `Node` → has many → `Post`

[Install](https://github.com/staudenmeir/eloquent-has-many-deep/#installation) the additional package, add the `HasRelationships` trait to the recursive model and [define](https://github.com/staudenmeir/eloquent-has-many-deep/#concatenating-existing-relationships) a deep relationship:

```
class Node extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasGraphRelationships;

    public function descendantPosts(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeepFromRelations(
            $this->descendants(),
            (new static)->posts()
        );
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

$descendantPosts = Node::find($id)->descendantPosts;
```

At the moment, recursive relationships can only be at the beginning of deep relationships:

- Supported: `Node` → descendants → `Node` → has many → `Post`
- Not supported: `Post` → belongs to → `Node` → descendants → `Node`

#### Known Issues

[](#known-issues-1)

MariaDB [doesn't yet support](https://jira.mariadb.org/browse/MDEV-19077) correlated CTEs in subqueries. This affects queries like `Node::whereHas('descendants')` or `Node::withCount('descendants')`.

### Package Conflicts

[](#package-conflicts)

- `staudenmeir/eloquent-param-limit-fix`: Replace both packages with [staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list](https://github.com/staudenmeir/eloquent-param-limit-fix-x-laravel-adjacency-list)to use them on the same model.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](.github/CONTRIBUTING.md) and [CODE OF CONDUCT](.github/CODE_OF_CONDUCT.md) for details.

###  Health Score

75

—

ExcellentBetter than 100% of packages

Maintenance85

Actively maintained with recent releases

Popularity69

Solid adoption and visibility

Community41

Growing community involvement

Maturity91

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 91.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~25 days

Recently: every ~54 days

Total

100

Last Release

78d ago

Major Versions

v1.26 → 8.5.x-dev2026-03-01

PHP version history (11 changes)v1.0PHP &gt;=7.0

v1.1PHP ^7.1.3

v1.2PHP ^7.2

v1.4PHP ^7.2.5

v1.6PHP ^7.3

v1.6.1PHP ^7.3|^8.0

v1.10PHP ^8.0.2

1.3.x-devPHP ^7.2.5|^8.0

v1.13PHP ^8.1

v1.21PHP ^8.2

v1.26PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/3c1c3cda28d147c1899037c3187f89a606ca14f51190de59e88fa91e51647ff6?d=identicon)[staudenmeir](/maintainers/staudenmeir)

---

Top Contributors

[![staudenmeir](https://avatars.githubusercontent.com/u/1853169?v=4)](https://github.com/staudenmeir "staudenmeir (244 commits)")[![SanderMuller](https://avatars.githubusercontent.com/u/9074391?v=4)](https://github.com/SanderMuller "SanderMuller (6 commits)")[![vanodevium](https://avatars.githubusercontent.com/u/16780069?v=4)](https://github.com/vanodevium "vanodevium (2 commits)")[![calebdw](https://avatars.githubusercontent.com/u/4176520?v=4)](https://github.com/calebdw "calebdw (1 commits)")[![daniel-de-wit](https://avatars.githubusercontent.com/u/3015394?v=4)](https://github.com/daniel-de-wit "daniel-de-wit (1 commits)")[![decadence](https://avatars.githubusercontent.com/u/1434668?v=4)](https://github.com/decadence "decadence (1 commits)")[![l3aro](https://avatars.githubusercontent.com/u/25253808?v=4)](https://github.com/l3aro "l3aro (1 commits)")[![loribean](https://avatars.githubusercontent.com/u/68610463?v=4)](https://github.com/loribean "loribean (1 commits)")[![nandi95](https://avatars.githubusercontent.com/u/41805560?v=4)](https://github.com/nandi95 "nandi95 (1 commits)")[![philipboeken](https://avatars.githubusercontent.com/u/32740847?v=4)](https://github.com/philipboeken "philipboeken (1 commits)")[![Propaganistas](https://avatars.githubusercontent.com/u/6680176?v=4)](https://github.com/Propaganistas "Propaganistas (1 commits)")[![raustin-m](https://avatars.githubusercontent.com/u/110375847?v=4)](https://github.com/raustin-m "raustin-m (1 commits)")[![Rinnsy](https://avatars.githubusercontent.com/u/68080844?v=4)](https://github.com/Rinnsy "Rinnsy (1 commits)")[![tylernathanreed](https://avatars.githubusercontent.com/u/6486381?v=4)](https://github.com/tylernathanreed "tylernathanreed (1 commits)")[![adrianpaiva1](https://avatars.githubusercontent.com/u/19557611?v=4)](https://github.com/adrianpaiva1 "adrianpaiva1 (1 commits)")[![whoami15](https://avatars.githubusercontent.com/u/16274153?v=4)](https://github.com/whoami15 "whoami15 (1 commits)")[![ariaieboy](https://avatars.githubusercontent.com/u/15873972?v=4)](https://github.com/ariaieboy "ariaieboy (1 commits)")[![Bloemendaal](https://avatars.githubusercontent.com/u/1348606?v=4)](https://github.com/Bloemendaal "Bloemendaal (1 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

### Embed Badge

![Health badge](/badges/staudenmeir-laravel-adjacency-list/health.svg)

```
[![Health](https://phpackages.com/badges/staudenmeir-laravel-adjacency-list/health.svg)](https://phpackages.com/packages/staudenmeir-laravel-adjacency-list)
```

###  Alternatives

[staudenmeir/eloquent-has-many-deep

Laravel Eloquent HasManyThrough relationships with unlimited levels

2.9k13.2M39](/packages/staudenmeir-eloquent-has-many-deep)[owen-it/laravel-auditing

Audit changes of your Eloquent models in Laravel

3.4k33.0M95](/packages/owen-it-laravel-auditing)[staudenmeir/eloquent-json-relations

Laravel Eloquent relationships with JSON keys

1.1k5.8M24](/packages/staudenmeir-eloquent-json-relations)[bavix/laravel-wallet

It's easy to work with a virtual wallet.

1.3k1.1M11](/packages/bavix-laravel-wallet)[dragon-code/migrate-db

Easy data transfer from one database to another

15717.4k](/packages/dragon-code-migrate-db)[gearbox-solutions/eloquent-filemaker

A package for getting FileMaker records as Eloquent models in Laravel

6454.8k2](/packages/gearbox-solutions-eloquent-filemaker)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
