akemys/baum
Baum brings the nested set model to Laravel Eloquent, making hierarchical data like categories, menus, and org charts easy to manage. Move nodes, query ancestors and descendants, and rebuild trees with a clean, tested API.
Version 1.0.1 is a small, non-breaking follow-up to the 1.0 line. The only change — "move with scope opt." — means the move operation now accepts an optional scope argument, letting you move a node/record into a different scope (tenant, tree root, category, locale, etc.) rather than only reordering within its current one.
Minimal steps:
composer update to pull 1.0.1. No migration or config publish is implied by these notes, but re-run php artisan vendor:publish --tag=<package>-config only if the config file is new to you.move() and read the method signature first — that is the entire surface area of this release. Confirm where the new optional scope parameter sits in the argument list.// Before: move stayed inside the node's existing scope
$node->move($targetParent);
// After: opt in to a different scope explicitly
$node->move($targetParent, scope: $otherTenant);
Start in the package's tests for move — they double as the clearest documentation of the new argument's accepted types and defaults.
Backwards-compatible default. Treat the scope parameter as "inherit current scope" when omitted. Wrap your existing move calls in a small domain method so the day you need cross-scope moves you change one place, not fifty:
public function relocate(Node $node, ?Node $parent = null, ?Scope $scope = null): void
{
DB::transaction(fn () => $node->move($parent, $scope));
}
Explicit-scope service methods. Rather than sprinkling the new argument through controllers, expose intent-named actions (moveToTenant(), promoteToRootTree()) that pass the scope explicitly. This keeps the optional argument from becoming an accidental third positional value that callers pass wrong.
Reordering vs. relocating. Separate the two workflows in your code. Sibling reordering should not pass a scope — passing one invites a no-op or an unintended cross-scope hop. Cross-scope relocation should always pass it, even if it equals the current scope, so the intent is explicit and readable in review.
Transactional + locked for concurrent moves. Tree/bound updates touch multiple rows. Always wrap in DB::transaction() and consider lockForUpdate() on the affected subtree rows when moves can be triggered by queued jobs or multiple users simultaneously. Reordering is a classic source of duplicate or gapped bounds under concurrency.
Queue large relocations. Moving a node with many descendants can be expensive. Dispatch a job, and expose a moved-style event or model observer so downstream indexes, search documents, and caches are invalidated after commit — not before.
Testing pattern. Use RefreshDatabase and assert on the observable structure after the move (parent id, scope id, depth/ordering columns) rather than on internal SQL. Include one test per direction: same-scope reorder, cross-scope move, and a move where the scope argument is omitted entirely.
$node->refresh()->scope_id === $targetScope->id.move(), they now need the extra parameter or they'll be incompatible. Grep for function move( across your app and any first-party extensions.move($a, $b) may now bind $b to the new scope parameter if it was previously a different second argument. Audit existing call sites with two or more arguments before deploying.refresh()/load() or unsetRelation(); if you cache rendered trees, key the cache on a tree version bumped after every move.moving/saving observer is short-circuiting. If bounds look corrupted, re-run the package's tree-fix/rebuild command rather than hand-patching columns.move() in your model/app subclass, call parent::move(...), and layer auditing, event dispatch, or scope-defaulting there. That keeps you on the upgrade path for 1.0.x patches."^1.0.1" and re-read the source diff between patch releases until the docs catch up.How can I help you explore Laravel packages today?