Eloquent Relations
NodeTrait registers four relations on every node:
| Relation | Type | Returns |
|---|---|---|
parent |
BelongsTo |
The immediate parent, or null for a root |
children |
HasMany |
Immediate children (one level down) |
ancestors |
custom (eager-loadable) | Every node above this one |
descendants |
custom (eager-loadable) | Every node below this one |
$laptops->parent->name; // 'Computers'
$computers->children->pluck('name'); // ['Laptops', 'Desktops']
$laptops->ancestors->pluck('name');
// → ['Electronics', 'Computers'] — ordered root-to-parent
$electronics->descendants->pluck('name');
// → ['Computers', 'Laptops', 'Desktops', 'Phones', 'Android']
Eager loading
The custom relations work with with(...) and load in two queries
total — no N+1, regardless of how many rows you select:
Category::with('ancestors')->get(); // breadcrumbs for every row, 2 queries
Category::with('descendants')->get(); // subtree for every row, 2 queries
whereHas and withCount work too:
Category::whereHas('descendants', fn ($q) => $q->where('active', true))->get();
Category::withCount('descendants')->get(); // each row gets descendants_count
Bounding the descendants relation
The descendants relation is unbounded by default — it pulls every
descendant of every selected row. For trees with deep, wide subtrees
this can be a lot more data than the UI needs. Bound the load to the
first N levels by composing a where on the relation's depth column
(which the trait already maintains):
// Just children + grandchildren of $root (depth 1 + 2 relative to root)
$root->load([
'descendants' => fn ($q) => $q->where('depth', '<=', $root->depth + 2),
]);
// Or on a top-level query — load every root with its first two levels
Category::with([
'descendants' => fn ($q) => $q->where('depth', '<=', 2),
])->whereIsRoot()->get();
The composite index already covers depth, so the bounded WHERE
costs no more than the unbounded eager load on the same rows.
Combining with tree query scopes
Relations stack with the tree-query scopes freely. This pattern is common for category-tree pages: load every root with its first two levels, ordered for display:
$tree = Category::query()
->whereIsRoot()
->with(['descendants' => fn ($q) => $q->where('depth', '<=', 2)->defaultOrder()])
->defaultOrder()
->get();