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

arcanedev/laravel-nestedset

Laravel Nested Set offers an efficient way to store and query hierarchical data using the nested set model, with a fluent API for creating, moving, and traversing trees and fast ancestor/descendant lookups without recursive queries.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're installing first. arcanedev/laravel-nestedset is an old (last release Feb 2017) wrapper/fork of the lazychaser/kalnoy nested-set model. The API you'll write against day-to-day is the familiar NodeTrait API: _lft, _rgt, parent_id columns, saveAsRoot(), appendToNode(), getDescendantsAndSelf(), toTree(), and scopes like whereIsRoot(). Because it's a thin wrapper, most upstream Kalnoy documentation applies, and most of your debugging will be against the underlying trait rather than Arcanedev's package code.

Minimal setup:

composer require arcanedev/laravel-nestedset

Check composer.json immediately after install — it will likely pull kalnoy/nestedset (or lazychaser/laravel-nestedset) as a dependency, and that is the version you actually care about for compatibility.

Add columns to your table (via migration):

Schema::table('categories', function (Blueprint $table) {
    $table->unsignedInteger('parent_id')->nullable()->index();
    $table->unsignedInteger('_lft')->default(0)->index();
    $table->unsignedInteger('_rgt')->default(0)->index();
});

Wire up the model:

use Arcanedev\NestedSet\NodeTrait;

class Category extends Model
{
    use NodeTrait;

    protected $fillable = ['name', 'parent_id'];
}

If Arcanedev's package does not expose its own trait name in your installed version, use Kalnoy\NestedSet\NodeTrait; is the fallback — verify in vendor/arcanedev/laravel-nestedset/src/.

First use case to prove it works — a category tree:

$root = Category::create(['name' => 'Root']);
$root->saveAsRoot();

$child = new Category(['name' => 'Child']);
$child->appendToNode($root)->save();

Category::whereIsRoot()->first()->getDescendantsAndSelf()->toTree();

If toTree() returns a flat array or bounds are wrong, jump straight to Gotchas — you almost certainly have an out-of-sync tree, not an API misuse.

Implementation Patterns

Building a tree bottom-up vs top-down. Two valid styles; pick one per codebase so reviewers don't have to reason about both:

// Top-down: create the parent, then append children
$child->appendToNode($parent)->save();

// Bottom-up: create everything flat, then attach
$child->save();
$child->makeChildOf($parent);   // also updates parent_id + bounds in one go

appendToNode() returns $this (fluent) but requires a subsequent save(); makeChildOf() persists immediately. Mixing them up is the #1 source of orphaned nodes.

Ordered sibling insertion. For menus and CMS taxonomies you almost never want "append to end":

$node->insertBefore($sibling);
$node->insertAfter($sibling);
$node->prependToNode($parent);

Use these instead of manually fiddling with _lft/_rgt. Sort order in toTree() defaults to _lft, so insertion order == display order for free.

Scoped trees (multi-tenant). Rarely used but critical when you need it — e.g. one category tree per store:

class Category extends Model
{
    use NodeTrait;
    protected $guarded = ['store_id'];
}

Category::scoped(['store_id' => $store->id])->saveAsRoot();
Category::scoped(['store_id' => $store->id])->whereIsRoot()->get()->toTree();

Remember to pass the scope on every call, including getDescendantsAndSelf().

Reading trees for a view layer. Three shapes, pick deliberately:

// Nested arrays with ->children — good for <ul> recursion
$tree = Category::whereIsRoot()->get()->toTree();

// Flat list with ->depth — good for indented <select> dropdowns
$flat = Category::get()->toFlatTree();

// Subtree only — good for category detail pages
$subtree = $category->getDescendantsAndSelf()->toTree();

Avoid getDescendants()->toTree() on a node — it returns descendants as roots of their own forest and drops the parent linkage you probably wanted.

Move operations in transactions. A move rewrites many rows; wrap it:

DB::transaction(function () use ($node, $target) {
    $node->moveTo($target, 'child'); // 'child', 'left', 'right'
});

For UI-level drag-and-drop, translate the drop event into moveTo($target, $position) and nothing else — do not compute bounds client-side.

Bulk loading without N+1. toTree() is efficient, but rendering $node->parent->name in a loop is not:

Category::with('parent')->whereIsRoot()->get()->toTree();

Better still, use withDepth() on the query and read $node->depth in views rather than walking parents.

Rebuilding after imports / raw SQL. If you seed trees with DB::table('categories')->insert(...) or any query-builder mass update, bounds are junk. Always finish with:

Category::rebuild();
// or per-scope:
Category::scoped(['store_id' => $id])->where('store_id', $id)->get()->toTree(); // verify

Run Category::isBroken() / Category::countErrors() in a test assertion after every import — that's your cheapest regression net.

Gotchas and Tips

Laravel/PHP compatibility is the real risk. A 2017 release targets Laravel 5.3–5.5 and PHP 7.0/7.1. On Laravel 8+/PHP 8 you will hit typed-property, Arr/Str helper, and Model::resolveRelationUsing signature changes inside the trait. Do not sink hours into compatibility shims — since this package is a wrapper, point Composer at kalnoy/nestedset directly and keep the same API. That's the single most important decision in this assessment. Treat Arcanedev's package as a historical snapshot, not a maintained dependency.

Nested sets assume exclusive write access. Two concurrent appendToNode()->save() calls can corrupt the whole tree, not just the two nodes involved. Lock the table (or the scoped subset) inside the transaction for writes that move nodes:

DB::transaction(function () { Category::lockForUpdate()->get(); /* then move */ });

For high-write models, nested sets are the wrong data structure — a closure table or parent_id-only adjacency list with recursive CTEs is safer.

Soft deletes silently break _lft/_rgt invariants. Deleting a node soft-deletes it but does not collapse the gap. If your queries scope out trashed rows, getDescendants() can return a subtree whose parent looks missing. Either hard-delete leaf nodes, or call ->delete() inside a move/rebuild routine, and always re-verify with isBroken().

Mass updates bypass the trait. Category::where('parent_id', $x)->update([...]) will happily destroy your tree because it never touches _lft/_rgt. Same for ->insert() and ->truncate(). Grep for these in code review; forbid them on nested-set models or pair them with an explicit rebuild().

Column naming is configurable, so read config/nestedset.php before debugging. If someone renamed _lft→lft in config, every Stack Overflow answer you find will be misleading. $node->getLftName() and $node->getRgtName() tell you the truth at runtime.

Indexes matter more than you think. Without a composite index on (_lft, _rgt), getDescendants() degrades badly on large tables. The migration snippet above adds single-column indexes; add the composite one if your tree exceeds a few thousand rows.

Debugging toolbelt — keep these four one-liners handy:

dd($node->getBounds());                    // lft/rgt sanity check on one node
Category::isBroken();                      // global tree integrity
Category::countErrors();                   // number of bad nodes
$node->getAncestors()->pluck('name');      // confirm parent chain

When toTree() "does nothing," it's almost always isBroken() === true from a prior mass update — fix the data, don't rewrite the query.

Extension point worth knowing: the trait fires moving/moved model events (per Kalnoy). That's the correct hook for cache invalidation or audit logging on tree changes — not the saved event, which fires per-row during a move and will flood your listeners.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core