Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/staudenmeir/laravel-adjacency-list
Recursive Laravel Eloquent relationships with CTEs
https://github.com/staudenmeir/laravel-adjacency-list
Last synced: 29 days ago
JSON representation
Recursive Laravel Eloquent relationships with CTEs
- Host: GitHub
- URL: https://github.com/staudenmeir/laravel-adjacency-list
- Owner: staudenmeir
- License: mit
- Created: 2019-04-24T01:50:25.000Z (over 5 years ago)
- Default Branch: main
- Last Pushed: 2024-10-01T20:15:42.000Z (about 1 month ago)
- Last Synced: 2024-10-01T23:02:35.918Z (about 1 month ago)
- Language: PHP
- Size: 377 KB
- Stars: 1,333
- Watchers: 25
- Forks: 109
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Contributing: .github/CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: .github/CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# 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://codecov.io/gh/staudenmeir/laravel-adjacency-list/graph/badge.svg?token=VhZ3oBh1YE)](https://codecov.io/gh/staudenmeir/laravel-adjacency-list)
[![PHPStan](https://img.shields.io/badge/PHPStan-level%209-brightgreen.svg?style=flat)](https://github.com/staudenmeir/laravel-adjacency-list/actions/workflows/static-analysis.yml?query=branch%3Amain)
[![Latest Stable Version](https://poser.pugx.org/staudenmeir/laravel-adjacency-list/v/stable)](https://packagist.org/packages/staudenmeir/laravel-adjacency-list)
[![Total Downloads](https://poser.pugx.org/staudenmeir/laravel-adjacency-list/downloads)](https://packagist.org/packages/staudenmeir/laravel-adjacency-list/stats)
[![License](https://poser.pugx.org/staudenmeir/laravel-adjacency-list/license)](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
- 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))
- Firebird## 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
| Laravel | Package |
|:--------|:--------|
| 11.x | 1.21 |
| 10.x | 1.13 |
| 9.x | 1.12 |
| 8.x | 1.9 |
| 7.x | 1.5 |
| 6.x | 1.3 |
| 5.8 | 1.1 |
| 5.5–5.7 | 1.0 |## 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)
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 & Recursive Query Constraints](#initial--recursive-query-constraints)
- [Additional Methods](#additional-methods)
- [Custom Relationships](#custom-relationships)
- [Deep Relationship Concatenation](#deep-relationship-concatenation)
- [Known Issues](#known-issues)#### Getting Started
Consider the following table schema for hierarchical data in trees:
```php
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:
```php
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()`:
```php
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()`:```php
class User extends Model
{
public function getLocalKeyName()
{
return 'id';
}
}
```#### 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.```php
$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
The trait provides the `tree()` query scope to get all models, beginning at the root(s):
```php
$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:```php
$constraint = function ($query) {
$query->whereNull('parent_id')->where('list_id', 1);
};$tree = User::treeOf($constraint)->get();
```You can also pass a maximum depth:
```php
$tree = User::tree(3)->get();$tree = User::treeOf($constraint, 3)->get();
```You can also chaperone tree relations to load parent/ancestor relations already present in the tree to (potentially) reduce 1+n queries:
```php
$tree = User::tree(3)->get();$tree->loadTreePathRelations();
```Or via `toTree`:
```php
$users = User::tree(1)->get();$tree = $users->loadTreePathRelations()->toTree();
```#### 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.```php
$noLeaves = User::hasChildren()->get();$noRoots = User::hasParent()->get();
$leaves = User::isLeaf()->get();
$leaves = User::doesntHaveChildren()->get();$roots = User::isRoot()->get();
```#### Order
The trait provides query scopes to order models breadth-first or depth-first:
- `breadthFirst()`: Get siblings before children.
- `depthFirst()`: Get children before siblings.```php
$tree = User::tree()->breadthFirst()->get();$descendants = User::find($id)->descendants()->depthFirst()->get();
```#### 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:```php
$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:
```php
class User extends Model
{
public function getDepthName()
{
return 'depth';
}
}
```##### Depth Constraints
You can use the `whereDepth()` query scope to filter models by their relative depth:
```php
$descendants = User::find($id)->descendants()->whereDepth(2)->get();$descendants = User::find($id)->descendants()->whereDepth('<', 3)->get();
```Queries with `whereDepth()` constraints that limit the maximum depth still build the entire (sub)tree internally.
Use `withMaxDepth()` to set a maximum depth that improves query performance by only building the requested section of
the tree:```php
$descendants = User::withMaxDepth(3, function () use ($id) {
return User::find($id)->descendants;
});
```This also works with negative depths (where it's technically a minimum):
```php
$ancestors = User::withMaxDepth(-3, function () use ($id) {
return User::find($id)->ancestors;
});
```#### 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:
```php
$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:
```php
class User extends Model
{
public function getPathName()
{
return 'path';
}
```You can also customize the path separator by overriding `getPathSeparator()`:
```php
class User extends Model
{
public function getPathSeparator()
{
return '.';
}
}
```#### Custom Paths
You can add custom path columns to the query results:
```php
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:
```php
class User extends Model
{
public function getCustomPaths()
{
return [
[
'name' => 'reverse_slug_path',
'column' => 'slug',
'separator' => '/',
'reverse' => true,
],
];
}
}
```#### Nested Results
Use the `toTree()` method on a result collection to generate a nested tree:
```php
$users = User::tree()->get();$tree = $users->toTree();
```This recursively sets `children` relationships:
```json
[
{
"id": 1,
"children": [
{
"id": 2,
"children": [
{
"id": 3,
"children": []
}
]
},
{
"id": 4,
"children": [
{
"id": 5,
"children": []
}
]
}
]
}
]
```#### 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:```php
$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()`.#### 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.```php
$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
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
Consider a `HasMany` relationship between `User` and `Post`:
```php
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:
```php
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:
```php
class User extends Model
{
public function descendantPosts()
{
return $this->hasManyOfDescendants(Post::class);
}
}
```##### BelongsToManyOfDescendants
Consider a `BelongsToMany` relationship between `User` and `Role`:
```php
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:
```php
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:
```php
class User extends Model
{
public function descendantRoles()
{
return $this->belongsToManyOfDescendants(Role::class);
}
}
```##### MorphToManyOfDescendants
Consider a `MorphToMany` relationship between `User` and `Tag`:
```php
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:
```php
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:
```php
class User extends Model
{
public function descendantTags()
{
return $this->morphToManyOfDescendants(Tag::class, 'taggable');
}
}
```##### MorphedByManyOfDescendants
Consider a `MorphedByMany` relationship between `Category` and `Post`:
```php
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:
```php
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:
```php
class Category extends Model
{
public function descendantPosts()
{
return $this->morphedByManyOfDescendants(Post::class, 'categorizable');
}
}
```##### Intermediate Scopes
You can adjust the descendants query (e.g. child users) by adding or removing intermediate scopes:
```php
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('<=', 10);
}
)->get();User::find($id)->recursivePosts()->withoutIntermediateScope('active')->get();
```##### Usage outside of Laravel
If you are using the package outside of Laravel or have disabled package discovery for `staudenmeir/laravel-cte`, you
need to add support for common table expressions to the related model:```php
class Post extends Model
{
use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions;
}
```#### Deep Relationship Concatenation
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). This
works with `Ancestors`, `Bloodline` and `Descendants` relationships (Laravel 9+).Consider a `HasMany` relationship between `User` and `Post` and building a deep relationship to get all posts of a
user's descendants:`User` → descendants → `User` → 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:```php
class User extends Model
{
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;public function descendantPosts(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
return $this->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
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)
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 & Recursive Query Constraints](#graphs-initial--recursive-query-constraints)
- [Deep Relationship Concatenation](#graphs-deep-relationship-concatenation)
- [Known Issues](#graphs-known-issues)#### Getting Started
Consider the following table schema for storing directed graphs as nodes and edges:
```php
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:```php
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()`:```php
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()`:```php
class Node extends Model
{
public function getLocalKeyName(): string
{
return 'id';
}
}
```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.```php
$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
Similar to `BelongsToMany` relationships, you can retrieve additional columns from the pivot table besides the parent
and child key:```php
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
If your graph contains cycles, you need to enable cycle detection to prevent infinite loops:
```php
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:```php
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
The trait provides the `subgraph()` query scope to get the subgraph of a custom constraint:
```php
$constraint = function ($query) {
$query->whereIn('id', $ids);
};$subgraph = Node::subgraph($constraint)->get();
```You can pass a maximum depth as the second argument:
```php
$subgraph = Node::subgraph($constraint, 3)->get();
```#### Order
The trait provides query scopes to order nodes breadth-first or depth-first:
- `breadthFirst()`: Get siblings before children.
- `depthFirst()`: Get children before siblings.```php
$descendants = Node::find($id)->descendants()->breadthFirst()->get();$descendants = Node::find($id)->descendants()->depthFirst()->get();
```#### Depth
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:```php
$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:
```php
class Node extends Model
{
public function getDepthName(): string
{
return 'depth';
}
}
```##### Depth Constraints
You can use the `whereDepth()` query scope to filter nodes by their relative depth:
```php
$descendants = Node::find($id)->descendants()->whereDepth(2)->get();$descendants = Node::find($id)->descendants()->whereDepth('<', 3)->get();
```Queries with `whereDepth()` constraints that limit the maximum depth still build the entire (sub)graph internally.
Use `withMaxDepth()` to set a maximum depth that improves query performance by only building the requested section of
the graph:```php
$descendants = Node::withMaxDepth(3, function () use ($id) {
return Node::find($id)->descendants;
});
```This also works with negative depths (where it's technically a minimum):
```php
$ancestors = Node::withMaxDepth(-3, function () use ($id) {
return Node::find($id)->ancestors;
});
```#### Path
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:
```php
$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:
```php
class Node extends Model
{
public function getPathName(): string
{
return 'path';
}
}
```You can also customize the path separator by overriding `getPathSeparator()`:
```php
class Node extends Model
{
public function getPathSeparator(): string
{
return '.';
}
}
```#### Custom Paths
You can add custom path columns to the query results:
```php
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:
```php
class Node extends Model
{
public function getCustomPaths(): array
{
return [
[
'name' => 'reverse_slug_path',
'column' => 'slug',
'separator' => '/',
'reverse' => true,
],
];
}
}
```#### Nested Results
Use the `toTree()` method on a result collection to generate a nested tree:
```php
$nodes = Node::find($id)->descendants;$tree = $nodes->toTree();
```This recursively sets `children` relationships:
```json
[
{
"id": 1,
"children": [
{
"id": 2,
"children": [
{
"id": 3,
"children": []
}
]
},
{
"id": 4,
"children": [
{
"id": 5,
"children": []
}
]
}
]
}
]
```#### 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
node's descendants while skipping inactive nodes and their descendants:```php
$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
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:```php
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
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
- `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
Please see [CONTRIBUTING](.github/CONTRIBUTING.md) and [CODE OF CONDUCT](.github/CODE_OF_CONDUCT.md) for details.