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

Baum Laravel Package

ttbooking/baum

Baum is a Laravel package that brings Nested Set trees to Eloquent models, making it easy to store, query, and manipulate hierarchical data such as categories or menus. It provides efficient ancestor and descendant lookups plus simple node movement.

View on GitHub
Deep Wiki
Context7

Getting Started

v2.0.0 is a breaking release: the root namespace moved to TTBooking\Baum, so any model currently extending Baum\Node will fatal until updated. Treat the upgrade as a code-touch task, not just a composer update.

  1. Install / upgrade

    composer require ttbooking/baum:^2.0
    

    Check the package's composer.json for the exact PHP/Laravel floor — v2 modernized the codebase and added static analysis, so expect current PHP 8.x and a recent Laravel major. Pin accordingly in CI.

  2. Migrate the namespace first. Global search/replace across app/:

    • Baum\Node → TTBooking\Baum\Node
    • Baum\Move → TTBooking\Baum\Move
    • Baum\Extensions\... → TTBooking\Baum\Extensions\...

    Run php artisan list | grep baum before deploying anything scripted — the release removes the Baum command, so any install/deploy step that shelled out to it will break.

  3. Add the nested-set columns to the target table if you're starting fresh (v2 pushes you to own the migration rather than generate it):

    $table->unsignedBigInteger('parent_id')->nullable()->index();
    $table->unsignedInteger('lft')->nullable()->index();
    $table->unsignedInteger('rgt')->nullable()->index();
    $table->unsignedInteger('depth')->nullable()->index();
    // optional per-tree root column when scoping
    $table->unsignedBigInteger('root_id')->nullable()->index();
    
  4. Model

    use TTBooking\Baum\Node;
    
    class Category extends Node
    {
        protected $table = 'categories';
        protected $fillable = ['name', 'parent_id', 'lft', 'rgt', 'depth'];
        // override column names only if you renamed them
        // protected $parentColumn = 'parent_id';
    }
    
  5. First use case — a category tree you can render as a menu:

    $root = Category::create(['name' => 'Shop'])->makeRoot();
    Category::create(['name' => 'Shoes'])->makeChildOf($root);
    
    $tree = Category::getDescendantsAndSelf()->toHierarchy();
    

Look first at the model's column-name properties and the move methods — those are where 90% of day-to-day customization happens.

Implementation Patterns

Tree building and moving. Nested set is write-heavy, so all structure changes go through Move, never raw attribute updates:

$node->makeRoot();
$node->makeChildOf($parent);
$node->moveToLeftOf($sibling);
$node->moveToRightOf($sibling);
$node->moveTo($target, 'child'); // or 'left' / 'right'

Wrap multi-node restructuring in DB::transaction() and consider Model::withoutEvents() for imports, then rebuild once.

Reading. Use the relationship-style scopes rather than hand-rolled where clauses:

$node->children()->get();
$node->descendants()->get();
$node->ancestors()->get();
$node->siblings()->get();
$node->getRoot();
Category::all()->toHierarchy();      // nested arrays/collections
Category::all()->toFlatTree();       // flat, ordered by lft

Scoped trees. Multi-tenant or per-company trees are supported via a scope column; set it consistently or you will silently merge trees:

Category::setScoped(['company_id' => $companyId]);
// or declare protected $scoped = ['company_id'];

Any query built outside the scope helper must re-apply the scope manually.

Rendering / UI. Persisted depth makes menu rendering cheap — query where('depth', '<=', 2)->orderBy('lft') in one shot instead of recursing. For recursive UI components and eager loading, with('children') plus toHierarchy() on an already-ordered set is the fastest path.

Bulk import. Create the flat records, then either assign parent_id and rebuild from the parent column, or insert with correct lft/rgt computed in PHP. Never insert rows with null lft/rgt and expect reads to work — run the rebuild step (whether that's a package method or your own) as part of the import job.

Search/index sync. If observers currently update a search index or cache on save, keep that listener but also call it explicitly after a structural rebuild — see the gotcha below.

Testing. The package suite now uses Orchestra Testbench; mirror that in your app with RefreshDatabase, and assert tree integrity after every move test (lft/rgt/depth consistency) rather than only asserting parent IDs.

Gotchas and Tips

  • Namespace is the whole upgrade. The TTBooking\ prefix applies to every exported class, including traits and query extensions. Miss one use Baum\... in a seeder or a Nova/Filament resource and you get a class-not-found only at runtime in that code path.
  • The removed command breaks deploy scripts. Confirm whether baum:rebuild survived; if not, replace rebuild steps with a queued job or console command of your own that calls the model's rebuild path. Check php artisan list on a staging deploy.
  • Events are no longer fired during rebuild. This is the subtlest behavior change: if any listener on saved/moved maintained a denormalized field, cache, or search index, those side effects will simply not happen after a rebuild. Add an explicit post-rebuild hook.
  • UUID keys are supported (test model now uses HasUuids) — if your nodes use UUIDs, the parent_id (and root) columns must be uuid/char(36), and the model needs $keyType = 'string', $incrementing = false. lft, rgt, depth remain integers regardless.
  • Concurrency. Nested set rewrites a range of rows per operation. Concurrent moves on the same tree can corrupt lft/rgt. Serialize structural writes with a transaction plus a row lock on the tree root, or move them onto a single-queue job.
  • Never assign lft/rgt/depth by hand in forms or seeders; mass-assignment guards should include them in $guarded. Corruption shows up as duplicated lft values — add an integrity test.
  • toHierarchy() needs ordered input. Call it on results already ordered by lft, or on getDescendantsAndSelf(); otherwise siblings shuffle unpredictably.
  • Static analysis is now part of the package. If you subclass Node or override Move/query methods, expect stricter signatures and typed properties to surface previously-silent mismatches (return types, nullable params). Run PHPStan on your own tree after upgrading.
  • Soft deletes + nested set still need care: a soft-deleted node keeps its lft/rgt unless you handle it; decide whether deletes are structural (rebuild) or leaves-only, and test both.
  • Indexes matter. Ensure lft, rgt, depth, parent_id (and any scope column) are indexed; ancestors()/descendants() are range scans on lft/rgt and degrade quickly on large flat tables.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core