baril/bonsai
Closure Table pattern implementation for Laravel Eloquent that lets you store and query tree-like structures in a relational database. Provides fast ancestor and descendant lookups across all supported DBMSs, migration artisan commands, and an alternative to nested sets.
ancestorsOf(), descendantsOf(), withDepth(), withHeight(), maxDepth(), getDistanceTo(), and findCommonAncestorWith() as first-class scopes and methods. That directly enables features PMs usually defer: breadcrumbs, "browse this subtree" filters, subtree-scoped permissions or pricing, depth-limited navigation, and "related" recommendations based on shared ancestry — without a data-science or platform-level effort.bonsai:grow). A PM can greenlight a hierarchy-based feature as a spike rather than a committed platform investment, and validate with users before funding a bespoke tree service.bonsai:fix truncates and rebuilds closures from the canonical parent_id column, and bonsai:show prints the tree with a chosen label and depth. That means a corrupted tree is a repair command, not a data-migration incident — a meaningful reduction in on-call and support burden that PMs should weigh in the risk column.delete vs. deleteTree, hard delete vs. the package's SoftDeletes trait with restoreTree() / forceDeleteTree(), plus a TreeException when a parent disappears mid-restore. These are product policies ("what happens to a deleted category's children?") that the package forces you to answer deliberately rather than discover in production.baril/orderly package is a clean add-on. PM can decide "explicit ordering" as its own line item rather than folding it into the hierarchy epic.Adopt when most of these hold:
parent, children, siblings, ancestors, descendants) so feature teams don't need special tree knowledge.with('descendants') recursively hydrates children with no additional queries, which matters for export, sitemap, or nested-JSON API endpoints.TreeException on cycles and prevents deleting a node with children unless you opt in via deleteTree(). That is product-grade guardrail behavior, not just a query helper.Look elsewhere when:
kalnoy/nestedset is the incumbent alternative with far greater adoption — at the cost of a harder-to-mutate nested-set encoding.For executives: This is a small, focused open-source library that gives us tree and hierarchy capability — categories, folders, org structures, nested navigation — without building it ourselves. Integration is roughly a trait on a model and one generated migration, so a hierarchy feature can be scoped as a normal backlog item rather than a platform program. The material risk is supportability: it is maintained by one person with a small user base. We mitigate that because the package's own design treats the closure table as derived data — the parent_id column remains the source of truth and a single artisan command rebuilds the tree index from scratch. Worst case, we fork or replace the indexing layer without a data migration. Recommendation: adopt behind a thin internal abstraction, ship one hierarchy feature on it, and revisit the dependency at the next architecture review. If the requirement is a multi-parent graph rather than a tree, this is the wrong tool and we should say so now.
For engineering: Eloquent-native closure table implementation. BelongsToTree trait gives you parent/children/siblings/ancestors/descendants, plus scopes (onlyRoots, onlyLeaves, descendantsOf, withDepth), predicates (isAncestorOf, isDescendantOf, getDistanceTo), and mutations (graft, graftOnto, cut). bonsai:grow generates the closure migration, bonsai:fix rebuilds it, bonsai:show inspects it. Cycles and orphan-causing deletes throw TreeException; deleteTree() handles cascades. Soft deletes and explicit sibling ordering (baril/orderly) are supported. Eager-loading descendants recursively hydrates children in one query. Trade-off to own: O(depth) write amplification per structural change, and a single-maintainer dependency — pin the version, keep the tree behind a repository/service boundary, and rely on bonsai:fix as the recovery path.
How can I help you explore Laravel packages today?