staudenmeir/laravel-adjacency-list
Eloquent extension that adds recursive relationships for trees (one parent) and graphs (many parents) using SQL common table expressions. Traverse ancestors/descendants and other hierarchies across MySQL, MariaDB, Postgres, SQLite, and SQL Server.
Start by installing the package via Composer and adding the Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships trait to your Eloquent model (e.g., Category, MenuItem, OrganisationalUnit). The model must have a parent_id column (nullable for root nodes).
composer require staudenmeir/laravel-adjacency-list
Define a basic model:
use Illuminate\Database\Eloquent\Model;
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
class Category extends Model
{
use HasRecursiveRelationships;
protected $fillable = ['name', 'parent_id'];
}
Run the initial migration to create the table:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->unsignedBigInteger('parent_id')->nullable()->index();
$table->timestamps();
});
Your first use case: retrieve all descendants of a category with filtering:
$root = Category::find(1);
$nested = $root->descendants()->where('active', true)->get(); // Depth-first, eager-loaded
First thing to read: the README’s “Usage” section covers core methods like ancestors(), children(), descendantsAndSelf(), and path().
Note: This package now requires Laravel 10+ (Laravel 13 fully supported as of v1.26). Firebird is no longer supported; ensure your application targets MySQL, PostgreSQL, SQL Server, or SQLite ≥ 3.8.4.
Recursive relationships via Eloquent
$category->parent; // Single parent
$category->children; // Direct children (eager-loadable)
$category->ancestors; // All ancestors, root-to-leaf order
$category->descendants; // All descendants, depth-first
$category->descendants()->where('status', 'published')->get(); // Scoped
$category->ancestorsAndSelf(); $category->descendantsAndSelf();
$category->path(); // Returns path from root to current node as a collection
Query constraints and ordering
inNaturalOrder() to sort descendants by id within sibling groups, or orderByDepth() to sort by hierarchy level:
$root->descendants()->orderByDepth()->get();
$root->children()->inNaturalOrder()->get();
Nest structures efficiently
descendants() with groupBy('parent_id') or use toTree() to convert results into nested arrays:
$tree = Category::query()->toTree(); // Returns fully nested tree
// Or for filtered trees:
$tree = Category::where('active', true)->toTree();
Conditional recursive scopes
whereRecursive():
Category::where('slug', 'electronics')
->descendants()
->whereRecursive(function ($query) {
$query->where('visible', true);
})
->get();
Cross-database portability
Database compatibility
Support for Firebird has been dropped in v1.26. If you rely on Firebird, pin to v1.25.x. For production, prefer PostgreSQL or MySQL 8.0+.
Laravel 13 compatibility
This release adds explicit support for Laravel 13. Ensure your composer.json allows laravel/framework ^13.0. No breaking API changes were introduced, but the minimum supported Laravel version is now 10 (previous versions <10 are unsupported).
Eager loading with nested constraints
Eager-loading relationships (e.g., Category::with('descendants')) applies to all descendants, but whereRecursive() won’t apply to the top-level model — only to the recursive part. To filter the whole tree, combine where() and whereRecursive().
Performance Gotcha: Depth Limits
By default, recursive queries are unbounded. For deep trees (e.g., >100 levels), set a max depth manually using where('depth', '<=', 10) after querying descendants (note: depth is not stored — it's computed per-query). Use orderByDepth() to group children by level.
Debugging recursive queries
Enable query logging (DB::enableQueryLog()) and inspect the with clause generated — it reveals how the package constructs CTEs. Look for laravel_recursive table aliases in the raw SQL.
Customizing relationships
If your parent_id column is named differently, override the getParentIdColumn() method:
public function getParentIdColumn()
{
return 'branch_id';
}
Scoping by path prefix
Need to scope descendants under a specific slug or path segment? Use whereRaw() with recursive column references:
$parentPath = Category::find(5)->path()->pluck('slug')->join('/');
Category::descendants()
->whereRaw("CONCAT('/', GROUP_CONCAT(slug) SEPARATOR '/') LIKE ?", ["%/$parentPath/%"])
->get();
… though usually ancestors()->where('slug', ...)->exists() suffices.
Avoid N+1 in trees
Use with('children') for shallow trees or with('descendants') for deep ones — but be mindful of result set size. For menus/org charts, prefer toTree() after fetching root nodes and applying loadMissing('children') where needed.
How can I help you explore Laravel packages today?