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.
Installation:
composer require staudenmeir/laravel-adjacency-list
Ensure your database driver (MySQL 8.0+, PostgreSQL 9.4+, etc.) supports CTEs.
Model Integration: Add the trait to your model:
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
class Category extends Model
{
use HasRecursiveRelationships;
}
First Query: Fetch a node and its descendants:
$category = Category::find(1);
$descendants = $category->descendants; // Recursive children
Tree Query: Fetch the entire tree from root nodes:
$tree = Category::tree()->get();
Hierarchical Data Fetching:
$model->ancestors (recursive parents).$model->descendants (recursive children).$model->bloodline (ancestors + descendants + self).Tree Construction:
$flatTree = Category::tree()->get();
$nestedTree = $flatTree->toTree();
$tree = Category::tree()->get()->loadTreeRelationships();
Filtering and Ordering:
$category->descendants()->whereDepth(2)->get();
$category->descendants()->breadthFirst()->get();
Custom Paths:
class Category extends Model
{
public function getCustomPaths()
{
return [
[
'name' => 'slug_path',
'column' => 'slug',
'separator' => '/',
],
];
}
}
Cycle Detection: Enable for cyclic graphs:
class Category extends Model
{
public function enableCycleDetection(): bool { return true; }
}
Eager Loading:
Use with() to avoid N+1 queries:
$categories = Category::with('descendants')->get();
Query Scopes: Filter trees dynamically:
Category::treeOf(function ($query) {
$query->where('is_published', true);
})->get();
Custom Relationships: Extend existing relationships (e.g., for polymorphic models):
class Category extends Model
{
public function hasManyOfDescendants(Post $post)
{
return $this->hasManyOfDescendantsAndSelf(Post::class);
}
}
Depth Optimization:
Use withMaxDepth() for large trees:
Category::withMaxDepth(3, function () {
return Category::find(1)->descendants;
})->get();
Path Manipulation: Reverse or customize paths for URLs:
$category->slug_path; // "parent/child"
$category->reverse_slug_path; // "child/parent"
Database Compatibility:
Cycle Detection Overhead:
Enabling enableCycleDetection() adds query complexity. Use only if cycles exist.
Depth Column Conflicts:
If your table already has a depth column, override getDepthName():
public function getDepthName() { return 'tree_depth'; }
Performance with Large Trees:
descendants() on deep trees without withMaxDepth().breadthFirst() for wide trees to limit memory usage.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',
];
Query Inspection:
Use Laravel’s query logging or toSql() to verify CTEs:
$query = Category::find(1)->descendants()->toSql();
Cycle Detection:
Check for is_cycle in results when enabled:
foreach ($model->descendants as $item) {
if ($item->is_cycle) {
// Handle cycle
}
}
Path Separator Issues:
Ensure separators (e.g., /) don’t conflict with data (e.g., URLs with / in slugs).
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);
});
}
}
Dynamic Relationships: Use closures for dynamic relationships:
public function dynamicDescendants($model)
{
return $this->hasManyOfDescendants($model);
}
Path Formatting:
Override getPathSeparator() or add custom path logic:
public function getCustomPaths()
{
return [
[
'name' => 'url_path',
'column' => 'slug',
'separator' => '-',
'format' => fn($path) => strtolower($path),
],
];
}
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');
}]);
}
Bulk Operations:
Use update() or delete() on relationships:
$category->descendants()->update(['visibility' => 'hidden']);
Depth-Based Logic: Filter by depth in queries:
$category->descendants()->whereDepth('>', 2)->get();
Tree Validation: Validate tree integrity with:
$category->isRoot(); // Check if no parent
$category->isLeaf(); // Check for children
Caching: Cache tree structures for read-heavy apps:
Cache::remember('tree', 60, function () {
return Category::tree()->get()->toTree();
});
Testing: Use factories to build test trees:
$root = Category::factory()->create();
$child = Category::factory()->create(['parent_id' => $root->id]);
How can I help you explore Laravel packages today?