ciaochung/baum
Baum provides nested set model support for Laravel Eloquent models, letting you store and query hierarchical tree data such as categories or menus with efficient ancestor, descendant, sibling, and depth operations.
Confirm the package is installable first. With 0 stars and 0 dependents this is a personal fork of baum/baum, so before writing any code:
composer show ciaochung/baum --all # is it on Packagist?
If it isn't published, add it as a VCS repository in composer.json:
"repositories": [{ "type": "vcs", "url": "https://github.com/ciao-chung/baum" }],
"require": { "ciaochung/baum": "dev-master" }
Then check composer.json inside the package for its illuminate/* constraint — Baum's classic API (Baum\Node extends Illuminate\Database\Eloquent\Model) was written for Laravel 5.x. Verify it boots on your Laravel version with a throwaway test before adopting it.
Migration. Three required columns plus the parent pointer (note lft/rgt, not left/right — those are MySQL reserved words):
Schema::create('categories', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->unsignedInteger('parent_id')->nullable();
$table->unsignedInteger('lft')->nullable()->index();
$table->unsignedInteger('rgt')->nullable()->index();
$table->unsignedInteger('depth')->nullable();
$table->timestamps();
});
Model. Extend Baum\Node instead of Model:
use Baum\Node;
class Category extends Node
{
protected $table = 'categories';
protected $fillable = ['name'];
// defaults: parent_id / lft / rgt / depth — override only if your schema differs
protected $parentColumn = 'parent_id';
protected $leftColumn = 'lft';
protected $rightColumn = 'rgt';
protected $depthColumn = 'depth';
protected $orderColumn = null;
}
First use case — build a tree in a seeder and read it back:
$root = (new Category(['name' => 'Root']))->saveAsRoot();
$hardware = (new Category(['name' => 'Hardware']))->makeChildOf($root);
$laptops = (new Category(['name' => 'Laptops']))->makeChildOf($hardware);
Category::getNestedList('name'); // [1 => 'Root', 2 => ' Hardware', 3 => ' Laptops']
$laptops->getAncestors(); // Root + Hardware, one query
$hardware->getDescendants(); // Laptops, one query
$laptops->getRoot()->name; // 'Root'
Where to look first in the repo: src/Baum/Node.php (moves, relations, scopes) and src/Baum/Move.php (the actual lft/rgt arithmetic). If the tree ever gets corrupted, that's the file that explains why.
Always mutate the tree through API methods, never by assignment. $node->parent_id = 5; $node->save(); will silently corrupt lft/rgt for the whole subtree. Use:
$node->makeChildOf($parent); // reparent
$node->makeSiblingOf($other); // move next to
$node->moveToLeftOf($other);
$node->moveToRightOf($other);
$node->moveToLeft(); // swap with previous sibling
$node->moveToRight();
Rendering a menu — two idiomatic options.
Recursive Blade partial (best when you need links, icons, per-node logic):
// Category.php
public function children() { return $this->hasMany(Category::class, 'parent_id'); }
// Blade
@foreach ($categories as $category)
@include('partials.category-node', ['category' => $category])
@endforeach
with @foreach ($category->children as $child) @include(...) @endforeach — eager load with Category::with('children')->whereNull('parent_id')->get() to avoid N+1.
Flat list (best for <select> and admin lists):
Category::getNestedList('name', 'id', '— '); // [id => '— — Laptops']
Scoped trees (the killer feature for multi-tenancy / per-project categories). Declare the scope columns and Baum keeps each tree independent automatically:
protected $scoped = ['company_id'];
Category::where('company_id', $id)->first()->getDescendants(); // stays inside the scope
Every query on a scoped model must carry the scope, otherwise sibling trees look merged.
Subtree moves for reordering, not per-node loops. A reorder drag-and-drop usually means one makeChildOf() per drop, which is fine — each is a bounded set of UPDATEs. Avoid rebuilding the entire tree on every save (see Gotchas).
Deletes: handle descendants yourself.
DB::transaction(function () use ($category) {
$category->getDescendants()->each->delete(); // leaves first
$category->delete();
});
Or re-parent children to the grandparent with $child->makeChildOf($category->parent) before deleting.
Composing with other relations. getDescendants() returns a normal Eloquent Collection, so pluck IDs for a second query instead of looping:
$ids = $category->getDescendantsAndSelf()->pluck('id');
Product::whereIn('category_id', $ids)->paginate(25);
Scoping helpers are handy in admin screens:
Category::withoutNode($node)->get(); // candidates for "move to…"
Category::withoutRoot()->get();
Category::withoutSelf()->get();
$node->getImmediateDescendants(); // direct children only
$node->getSiblings();
kalnoy/nestedset in your back pocket as the maintained alternative with a very similar API. Pin the commit hash (dev-master#<sha>) rather than a floating branch.lft/rgt naming is deliberate — MySQL reserves left/right. Don't "clean up" the column names unless you also override $leftColumn/$rightColumn.foreach without measuring.UPDATEs and shred the tree. Serialize structural writes (queue, lock, or SELECT … FOR UPDATE on the root row) if your admin UI can be used by more than one person.parent_id — if parent_id is in $fillable, a stray Category::create(['parent_id' => …]) bypasses the move logic. Keep it out of $fillable and set it only via makeChildOf().getDescendants() returns nothing for a node that visibly has children.(lft, rgt) and (parent_id); descendant queries are range scans on lft BETWEEN … AND …, and without the index a large catalog will crawl.lft/rgt should form properly nested intervals (child's lft > parent's lft, child's rgt < parent's rgt, depth = ancestor count). If a rebuild helper ships in this fork, verify it against a backup first — rebuilding is itself destructive if the parent pointers are wrong.moving/saved observer or a small subclass of Baum\Node, not patches to Move.php. Override getNestedList() for custom labels rather than post-processing the array.lft/rgt after every move type — that's where forks diverge. A quick property test ("for every node, lft < rgt and intervals nest") catches regressions fast.How can I help you explore Laravel packages today?