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 is compatible (MySQL 8.0+, PostgreSQL 9.4+, etc.).
Model Integration: Add the trait to your model:
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
class Category extends Model
{
use HasRecursiveRelationships;
}
First Query: Fetch a tree structure with roots:
$categories = Category::tree()->get();
Or load descendants of a specific node:
$category = Category::find(1);
$descendants = $category->descendants;
Hierarchical Data Fetching:
// Fetch a nested category tree with depth-limited children
$tree = Category::tree()->withMaxDepth(2)->get()->toTree();
Eager Loading with Constraints:
// Load descendants with eager-loaded relationships
$categories = Category::with(['descendants.posts'])->get();
Path-Based Filtering:
// Find nodes with a specific path (e.g., "1.2.3")
$node = Category::where('path', 'like', '1.2.%')->first();
Cycle-Safe Queries:
// Enable cycle detection for recursive queries
Category::enableCycleDetection();
$safeDescendants = Category::find(1)->descendants;
Custom Path Columns:
// Add slug-based paths to results
class Category extends Model
{
public function getCustomPaths()
{
return [
[
'name' => 'slug_path',
'column' => 'slug',
'separator' => '/',
],
];
}
}
toTree() to return nested JSON structures:
return response()->json($categories->toTree());
breadthFirst()/depthFirst() for intuitive tree visualization.withMaxDepth() to avoid recomputing large hierarchies:
Cache::remember('category-tree', now()->addHours(1), function () {
return Category::tree()->withMaxDepth(3)->get()->toTree();
});
Database Compatibility:
Cycle Detection Overhead:
enableCycleDetection() adds query complexity. Only enable it if cycles are possible.includeCycleStart() sparingly—it impacts performance.Depth Constraints:
whereDepth() filters after building the full subtree. Use withMaxDepth() for large trees:
// Bad (inefficient for deep trees)
$deepNodes = Category::find(1)->descendants()->whereDepth('>', 5)->get();
// Good (limits query scope)
$deepNodes = Category::withMaxDepth(10, function () {
return Category::find(1)->descendants;
})->whereDepth('>', 5)->get();
N+1 Queries:
toTree() creates nested arrays but doesn’t eager-load relationships. Use loadTreeRelationships():
$tree = Category::tree()->get()->loadTreeRelationships()->toTree();
Custom Path Conflicts:
getPathName() or getDepthName() may clash with existing columns. Use unique names:
public function getDepthName() { return 'tree_depth'; }
DB::enableQueryLog();
$tree = Category::tree()->get();
dd(DB::getQueryLog());
is_cycle in results when debugging infinite loops:
$descendants = Category::find(1)->descendants;
$cycles = $descendants->filter(fn ($node) => $node->is_cycle);
explain() to analyze query plans for slow recursive queries:
DB::connection()->enableQueryLog();
Category::find(1)->descendants()->get();
tap(DB::getQueryLog(), function ($logs) {
dd(DB::connection()->getPdo()->query('EXPLAIN ' . $logs[0]['query'])->fetchAll());
});
class Category extends Model
{
public function scopeActiveTree($query)
{
return $query->where('active', true)->tree();
}
}
getInitialQuery() or getRecursiveQuery() to inject constraints:
public function getInitialQuery($query)
{
return $query->where('published', true);
}
public function tags()
{
return $this->belongsToMany(Tag::class)->withPivot('order');
}
public function recursiveTags()
{
return $this->hasManyOfDescendantsAndSelf(Tag::class, 'tags');
}
How can I help you explore Laravel packages today?