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

vusys/laravel-nestedset

A modern Laravel nested-set implementation for hierarchical Eloquent data, featuring strict types, PHPStan level 9, atomic CASE-WHEN mutations, multi-tree scoping, and soft deletes. Supports PHP 8.3 and Laravel 11, 12, or 13.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package and confirm the Laravel version constraint matches your app:

    composer require vusys/laravel-nestedset
    
  2. Add the nested-set columns to the table that will hold the hierarchy. The minimum is a parent key plus the left/right boundaries:

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

    Run the migration before seeding any rows, otherwise the tree columns will not line up.

  3. Attach the node trait to your Eloquent model and import the interface/trait pair the package expects (usually a NodeTrait and the matching contract):

    use Vusys\LaravelNestedSet\NodeTrait;
    
    class Category extends Model
    {
        use NodeTrait;
    
        protected $fillable = ['name', 'parent_id'];
    }
    
  4. Where to look first: read the package's docs/ directory (or the README's API section) for the scope names (ancestors, descendants, siblings, leaves) and the move methods (appendToNode, prependToNode, insertBeforeNode, insertAfterNode, makeRoot). These are the surface you will use 90% of the time.

  5. First use case: a category/product tree. Seed roots with Category::create(['name' => 'Root']), then attach children:

    $root = Category::create(['name' => 'Electronics']);
    $root->children()->create(['name' => 'Phones']);
    

    Fetch the whole tree with Category::toTree() (or ->get()->toTree()) and render it recursively in a Blade partial.

Implementation Patterns

Render a tree in a controller. Load once, build in memory, avoid N+1:

$tree = Category::defaultOrder()->get()->toTree();
return view('categories.index', compact('tree'));

Then use a recursive Blade component that walks $node->children.

Scoped queries instead of full-tree loads. For detail pages, only pull the branch you need:

$category = Category::findOrFail($id);
$breadcrumbs = $category->ancestors()->defaultOrder()->get();
$subtree = $category->descendants()->defaultOrder()->get();

Building an admin drag-and-drop editor. Each drop maps to a move operation. Wrap it in a transaction and re-read the node:

DB::transaction(function () use ($node, $parent) {
    $node->appendToNode($parent)->save();
});
$node->refresh();

Return the affected subtree so the frontend can re-render without a full reload.

Multi-tree / per-tenant hierarchies. If several independent trees live in one table, add a discriminator column (e.g. tenant_id) and scope every query. The package's default order and move operations assume a single tree unless you constrain them; enforce this with a base scope on the model.

API resources. Use toTree() with a resource collection that recursively transforms children. Keep _lft/_rgt out of the JSON payload—they leak implementation detail and confuse clients.

Soft deletes. If your model uses SoftDeletes, ensure delete/restore operations still keep the tree consistent and that scopes exclude trashed nodes from descendants. Test deletions specifically—removing a node with children should either cascade or move children up, per your domain rules.

Seeding and bulk imports. For large imports, build rows flat and rebuild the tree columns in one pass rather than calling appendToNode per row (each move is a write-heavy operation).

Gotchas and Tips

  • Nested sets are write-heavy, read-fast. Every insert/move updates a range of _lft/_rgt values. Do not use this package for high-write workloads (e.g. live comment threads with constant inserts); prefer adjacency lists or closure tables there. Use it for mostly-static hierarchies like categories, menus, org charts.
  • Always wrap moves in a transaction. A failed move can leave the tree corrupted. If you support concurrent edits, lock the relevant rows (->lockForUpdate()) or guard with a queue.
  • Refresh after mutation. After appendToNode(...)->save(), the in-memory _lft/_rgt on the model and its ancestors are stale. Call $node->refresh() before reading ancestors()/descendants().
  • Never insert with raw SQL or insert()/upsert(). Direct inserts bypass the boundary recalculation and silently corrupt the tree. If you must, follow up with a tree rebuild (check the docs for a rebuild/fixTree-style command).
  • Watch the column names. The package expects specific boundary column names by default. If your schema uses lft/rgt instead of _lft/_rgt, override them via the documented properties/constants on the trait rather than renaming columns ad hoc.
  • Debugging a corrupt tree. Common symptoms: descendants() returns wrong nodes, toTree() drops branches. dd(Category::defaultOrder()->get(['id','parent_id','_lft','_rgt'])) to inspect boundaries, then run the rebuild command. Add a test that asserts parent/child relationships after every migration that touches the table.
  • Eager loading. ->with('children') only loads one level. For deep trees either use descendants scoping or recursively load; with('descendants') can be expensive on wide trees.
  • Config quirks. If the package ships a config file, publish it (php artisan vendor:publish) and check for the table/column conventions and any default_order toggle—surprises usually come from an unpublished config.
  • Maturity caution. This package has very low adoption (0 stars, 0 dependents). Treat it as an alternative to the well-established kalnoy/nestedset and pin the version in composer.json. Audit the docs/ and tests before committing; keep your own integration tests around tree moves and deletions so a future dependency swap is a drop-in change.
  • Extension points. Subclass the trait or override the boundary column constants, add custom scopes for tenant filtering, and hook model events (saving, deleted) if you need audit logging around moves—just don't reorder the tree inside those events.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console