Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Nestedset Laravel Package

aimeos/laravel-nestedset

Laravel nested set package for storing and querying hierarchical data as trees. Adds tree support to Eloquent models with fast ancestor, descendant, sibling, and depth queries via left/right indexes, plus simple APIs for moving nodes and rebuilding trees.

Deep Wiki
Context7

Getting Started

Install and wire it up in three steps.

composer require aimeos/laravel-nestedset
  1. Migration. The package registers a Blueprint macro via its (auto-discovered) service provider:
Schema::create('categories', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->nestedSet();      // adds _lft, _rgt, parent_id + index
    $table->timestamps();
});

If you prefer explicit columns, the macro is equivalent to nullable unsigned _lft, _rgt, parent_id plus an index on (_lft, _rgt) and parent_id.

  1. Model. Add the trait. Nothing else is required — the trait hooks the query builder, collection, and model events:
use Aimeos\Nestedset\NodeTrait;

class Category extends Model
{
    use NodeTrait;
}
  1. First use case. The canonical one is a category tree or menu. Create a root, hang children off it, read it back:
$root  = Category::create(['name' => 'Products']);
$shoes = Category::create(['name' => 'Shoes'])->appendToNode($root)->save();
Category::create(['name' => 'Sneakers'])->appendToNode($shoes)->save();

$root->refresh()->descendants;        // flat, ordered by _lft
Category::defaultOrder()->get()->toTree();   // nested with `children` loaded

Where to look first: the NodeTrait source itself (vendor/aimeos/laravel-nestedset/src/NodeTrait.php). It is small and is the authoritative reference for which move/query methods exist in your installed version. Then read the README section on Moving nodes and Retrieving nodes — those two cover 90% of daily use. If you are migrating from lazychaser/laravel-nestedset, the API is essentially identical; only the namespace (Aimeos\Nestedset) changed, so existing tutorials and StackOverflow answers still apply.

Implementation Patterns

Creating and attaching nodes. There is no parent_id assignment in practice — you position a node relative to another:

$node->appendToNode($parent)->save();     // last child
$node->prependToNode($parent)->save();    // first child
$node->insertBeforeNode($sibling)->save();
$node->insertAfterNode($sibling)->save();
$node->makeRoot()->save();                // new root
$existing->saveAsRoot();                  // promote an existing subtree

The save() is mandatory — the move methods stage the change, save() performs the _lft/_rgt rewrite.

Reading. Use the relations for one level, the get* helpers for the full set:

$node->children;                 // direct children
$node->parent;                   // parent model
$node->ancestors;                // root → parent, ordered
$node->descendants;              // whole subtree below, ordered by _lft
$node->getAncestorsAndSelf();
$node->getDescendantsAndSelf();
$node->getRoot();                // walk up

Turning a flat result into a tree is the single most-used pattern in views and APIs:

$tree = Category::defaultOrder()->get()->toTree();      // nested, `children` set
$flat = Category::defaultOrder()->get()->toFlatTree();  // flat, each item has `depth`

// Scoped to one subtree:
$tree = $node->descendants()->defaultOrder()->get()->toTree();

toTree() links nodes by parent_id (so the row must actually have it populated) and relies on _lft ordering to preserve sibling order. Always chain defaultOrder() — without it the DB returns rows in arbitrary order and your tree renders scrambled.

Select-list / dropdown pattern. toFlatTree() plus depth is the clean way to render an indented <select>:

foreach (Category::defaultOrder()->get()->toFlatTree() as $c) {
    echo str_repeat('— ', $c->depth) . $c->name;
}

Query scopes avoid materialising subtrees in PHP:

Category::whereDescendantOf($node)->get();       // excludes $node itself
Category::whereAncestorOf($node)->get();
Category::whereNotDescendantOf($node)->get();

Drag-and-drop reordering maps 1:1 onto insertBeforeNode / insertAfterNode / appendToNode:

DB::transaction(function () use ($moved, $target, $position) {
    match ($position) {
        'before' => $moved->insertBeforeNode($target),
        'after'  => $moved->insertAfterNode($target),
        default  => $moved->appendToNode($target),
    };
    $moved->save();
});

Eager loading. with('children') gives you one level; with('descendants') gives you the whole subtree in one query, and you then call toTree() on the collection:

$root = Category::with('descendants')->find($id);
$tree = $root->descendants->toTree();

Multi-tenant / multiple trees. Filter by your own scope column on every query and, if your fork supports it, override getScopeAttributes() to return e.g. ['menu_id'] so _lft/_rgt are computed per scope. Verify this against your installed source before relying on it — behaviour differs between forks. The safe fallback is a manual ->where('menu_id', $id) on reads and separate roots per menu.

Gotchas and Tips

  • Bulk writes silently corrupt the tree. Model::insert(), update() on _lft/_rgt, or mass parent_id updates bypass the trait. Any tree repair should be done with the package's rebuild helper (Model::fixTree(), or rebuildNestedSet() in newer forks) — check which exists in your version. Run it in a queued job on large tables.
  • Moves are expensive and lock ranges. Appending a node rewrites _lft/_rgt for every row to the right of the insertion point. On a 10k-node tree, dragging a node can touch thousands of rows. Wrap moves in a transaction, and on large trees queue them rather than doing them inline in a request.
  • Concurrent moves deadlock. Two simultaneous reorders can grab overlapping row ranges. If tree edits are frequent, serialise them (queue, or lockForUpdate on the scope) rather than fighting deadlocks in production.
  • whereDescendantOf() excludes the node itself. If you want the node too, use getDescendantsAndSelf() or add orWhere on the primary key. This trips people up in permission checks constantly.
  • In-memory _lft/_rgt go stale immediately after a move. Call $node->refresh() (or re-query) before doing further positional reads on the same instance.
  • Soft deletes leave holes. A soft-deleted node still occupies its _lft/_rgt range, so it keeps blocking sibling math and can appear in toTree() if your scope includes trashed rows. Decide explicitly whether deletes should be hard deletes for tree models.
  • Hard deletes cascade. The trait's delete() removes descendants — confirm the semantics in your installed version before wiring a "delete category" button to it, and consider requeueing children to the parent instead.
  • Column types matter. Use unsigned integers for _lft/_rgt and make sure both are indexed together. On very large trees, watch for overflow (4.29B ceiling for unsigned INT) — BIGINT is a cheap insurance policy.
  • toTree() needs the package collection. It lives on Aimeos\Nestedset\Collection. If you build results with DB::table(), a raw query, or ->pluck(), you get a plain Support\Collection and toTree() will not exist.
  • N+1 is easy to write. $node->children inside a Blade loop is a query per row. Prefer ->with('children') for shallow menus and ->descendants + toTree() for deep ones.
  • No config file. Column names are customisable only through the trait's getLftName() / getRgtName() / getParentIdName() hooks. If you override them, your migration must match — using the nestedSet() macro and custom names together is a common mismatch.
  • Extension point: the trait's events hook is where you add cache invalidation. Any tree-mutating operation should bust whatever cache holds your rendered tree; there is no built-in cache because invalidation depends entirely on your move frequency.
  • Adoption risk. 39 stars and a single maintainer means low bus factor, but the code is a direct descendant of the widely used lazychaser/laravel-nestedset, so the API surface is battle-tested. Pin the version in composer.json and keep a thin repository/service layer around tree mutations so a future swap is cheap.
  • Testing: SQLite handles the package fine for unit tests, but its locking behaviour differs from MySQL/Postgres — don't draw concurrency conclusions from SQLite tests.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
adachsoft/changelog-linter