A modern Laravel implementation of the nested-set model for hierarchical data — strict types throughout, PHPStan level 9, atomic CASE-WHEN mutations, multi-tree scoping, soft-delete cascade, live aggregate roll-ups (eager or lazy with TTL), subtree cloning, JSON tree import/export, materialised-path columns, and an opinionated repair toolkit.
use App\Models\BudgetItem;
$root = new BudgetItem(['name' => 'Engineering']);
$root->saveAsRoot();
$child = new BudgetItem(['name' => 'Salaries', 'cost' => 26000]);
$child->appendToNode($root)->save();
$child->depth; // 1
$child->isLeaf(); // true
$child->isDescendantOf($root); // true
$child->parent; // $root (Eloquent relation, eager-loadable)
$child->ancestors()->get(); // collection containing $root
$root->refresh(); // re-read parent bounds after the append
$root->descendants()->get(); // collection containing $child
$root->getDescendantCount(); // 1 (descendants, excluding self; +1 for total nodes in subtree)Declare aggregates on the model and the SUM / COUNT / AVG / MIN / MAX roll-ups are maintained automatically as the tree changes:
use Illuminate\Database\Eloquent\Model;
use Vusys\NestedSet\Attributes\NestedSetAggregate;
use Vusys\NestedSet\Contracts\HasNestedSet;
use Vusys\NestedSet\Export\AsciiOptions;
use Vusys\NestedSet\NodeTrait;
#[NestedSetAggregate(column: 'cost_total', sum: 'cost')]
#[NestedSetAggregate(column: 'item_count', count: true)]
#[NestedSetAggregate(column: 'avg_cost', avg: 'cost')]
#[NestedSetAggregate(column: 'biggest_item', max: 'cost')]
#[NestedSetAggregate(column: 'recurring_total', sum: 'cost', filter: ['recurring' => true])]
class BudgetItem extends Model implements HasNestedSet { use NodeTrait; }
// Render the forest with each node's own cost + rolled-up subtree total:
$render = fn () => BudgetItem::toAsciiTreeForest(new AsciiOptions(
label: fn ($n) => "{$n->name} (cost = {$n->cost}, total = {$n->cost_total})",
));
echo $render();
// Engineering (cost = 0, total = 32000)
// ├── People (cost = 0, total = 28000)
// │ ├── Salaries (cost = 26000, total = 26000)
// │ └── Bonuses (cost = 2000, total = 2000)
// └── Tools (cost = 0, total = 4000)
// ├── SaaS (cost = 2500, total = 2500)
// └── Hardware (cost = 1500, total = 1500)
// Operations (cost = 0, total = 1500)
// └── Office (cost = 1500, total = 1500)
BudgetItem::query()->where('name', '=', 'Bonuses')->first()->update(['cost' => 4000]);
BudgetItem::query()->where('name', '=', 'Engineering')->first()->refresh()->cost_total; // 34000 — every ancestor updates (Bonuses → People → Engineering)
// move the whole Tools subtree (3 nodes) under Operations — one statement
BudgetItem::query()->where('name', '=', 'Tools')->first()
->moveTo(BudgetItem::query()->where('name', '=', 'Operations')->first())
->save();
echo $render();
// Engineering (cost = 0, total = 30000) ← old parent shrank (Tools left, taking 4000)
// └── People (cost = 0, total = 30000)
// ├── Salaries (cost = 26000, total = 26000)
// └── Bonuses (cost = 4000, total = 4000)
// Operations (cost = 0, total = 5500) ← new parent grew by the whole moved subtree
// ├── Office (cost = 1500, total = 1500)
// └── Tools (cost = 0, total = 4000)
// ├── SaaS (cost = 2500, total = 2500)
// └── Hardware (cost = 1500, total = 1500)
BudgetItem::query()->withFreshAggregates()->get(); // ad-hoc correlated recomputation (read-only — see Aggregates → Drift)The nested-set encoding stores lft and rgt integers on every node so any subtree, ancestor chain, or descendant set is a single BETWEEN query — no recursive CTEs, no N+1 loops. The price is that mutations (insert / move / delete) have to shift many rows to keep the lft/rgt sequence dense, so it's best suited to read-heavy hierarchies: category trees, menu structures, org charts, comment threads.
This package executes every shift as a single CASE WHEN UPDATE, so even a subtree move that touches thousands of rows is one round trip.
composer require vusys/laravel-nestedsetThe service provider auto-registers Blueprint macros and registers a publishable config file:
php artisan vendor:publish \
--provider="Vusys\NestedSet\NestedSetServiceProvider" \
--tag=nestedset-configSee the Installation guide for the rest of the setup (migration macros, model trait, scoped trees).
Full documentation lives at https://vusys.github.io/laravel-nestedset/.
- Getting Started — Introduction · Installation · Migration · Primary Keys · Model Setup
- Tree Operations — Inserting & Moving · Reordering Siblings · Soft Deletes · Bulk Insertion · Cloning Subtrees · Materialised Paths
- Querying — Tree Queries · Eloquent Relations · In-memory Tree Shaping · Walking Subtrees · Tree Exporters & JSON Import · Inspection · Scoped Trees
- Aggregates — Overview · Setup · Reading · Declaring · Filtered · Collection · Listeners · Variance & Stddev · Weighted Avg & Booleans · Means · Quantiles · Bitwise · Lazy · Recipes · Maintenance · Drift & Limitations
- Maintenance — Tree Repair · Repairing Aggregates · Corruption Reference
- Reference — Configuration · Testing Helpers · Factory Tree Builder · Transactions · Events · Production Notes · Glossary
- Internals — Architecture Overview
The site is built from the markdown in docs/ — if you spot an error, edit the source and open a PR.
Run the full check suite before opening a PR:
composer pint:check # style
composer rector:check # automated refactors
composer analyse # static analysis
composer test # unit + featureAll four must pass on CI before merge.
MIT. See LICENSE.