## Getting Started
### Minimal Setup
1. **Install the package**:
```bash
composer require baril/bonsai
use Baril\Bonsai\Concerns\BelongsToTree;
class Category extends Model
{
use BelongsToTree;
// Customize if needed
protected $parentForeignKey = 'parent_id';
protected $closureTable = 'category_tree';
}
php artisan bonsai:grow App\Models\Category
php artisan migrate
php artisan bonsai:fix App\Models\Category
// Create a root node
$electronics = new Category(['name' => 'Electronics']);
$electronics->save();
// Create a child node
$smartphones = new Category(['name' => 'Smartphones']);
$smartphones->parent()->associate($electronics);
$smartphones->save();
// Query descendants
$smartphones->descendants()->get();
// Move a node to a new parent
$node->graftOnto($newParent);
// Detach a node (make it a root)
$node->cut();
// Delete a node and its descendants
$node->deleteTree();
// Get all descendants (with depth)
$node->descendants()->withDepth()->get();
// Get ancestors ordered by depth
$node->ancestors()->orderByDepth()->get();
// Find common ancestor
$node->findCommonAncestorWith($otherNode);
// Eager-load descendants (recursively)
$categories = Category::with('descendants')->get();
// Get siblings (excluding self)
$node->siblings()->get();
// Get siblings including self
$node->siblings()->withSelf()->get();
// Query only roots
Category::onlyRoots()->get();
// Query leaves (nodes with no children)
Category::onlyLeaves()->get();
// Query nodes with children
Category::hasChildren()->get();
use Baril\Bonsai\Concerns\SoftDeletes;
class Category extends Model
{
use BelongsToTree, SoftDeletes;
}
// Restore a soft-deleted node and its descendants
$node->restoreTree();
orderly)use Baril\Bonsai\Concerns\Orderable;
class Category extends Model
{
use BelongsToTree, Orderable;
protected $orderColumn = 'position';
}
// Query ordered children
$node->children()->ordered()->get();
class Category extends Model
{
use BelongsToTree;
protected $parentForeignKey = 'parent_category_id';
protected $closureTable = 'custom_category_tree';
}
// Move multiple nodes to a new parent
$nodes = Category::where('parent_id', $oldParent)->get();
foreach ($nodes as $node) {
$node->graftOnto($newParent);
}
Circular References
TreeException.Closure Table Sync
save(). It updates only when the parent_id changes.save() after modifying parent_id to ensure sync.Soft Deletes and Parentage
cut() before restoring if the parent is gone:
try {
$node->restore();
} catch (\Baril\Bonsai\TreeException $e) {
$node->cut()->restore();
}
Performance with Deep Trees
descendants() or ancestors() can be slow for very deep trees.maxDepth() to limit results:
$node->descendants()->maxDepth(3)->get();
Eager Loading Quirks
with('descendants') loads all descendants recursively by default. For large trees, this can be expensive.with(['descendants' => function($query) { ... }]) to constrain the query.Inspect the Closure Table
bonsai:show to visualize the tree structure:
php artisan bonsai:show App\Models\Category --label=name --depth=5
Check for Orphaned Closures
bonsai:fix to rebuild the closure table:
php artisan bonsai:fix App\Models\Category
Enable Query Logging
\DB::enableQueryLog();
$node->descendants()->get();
dd(\DB::getQueryLog());
Custom Scopes
class Category extends Model
{
use BelongsToTree;
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}
Override Tree Methods
getDepth()):
public function getDepth()
{
$depth = parent::getDepth();
return $depth + 1; // Custom logic
}
Add Custom Closure Columns
Schema::create('category_tree', function (Blueprint $table) {
$table->unsignedBigInteger('ancestor_id');
$table->unsignedBigInteger('descendant_id');
$table->unsignedInteger('depth');
$table->unsignedInteger('custom_field'); // Add your column
$table->primary(['ancestor_id', 'descendant_id']);
});
Event Listeners
saved, deleted) to trigger side effects:
$node->saved(function ($node) {
// Post-save logic
});
UUID Primary Keys
Multi-Database Setups
bonsai:grow command:
php artisan bonsai:grow App\Models\Category --connection=secondary
Case-Sensitive Table Names
$closureTable.Transaction Handling
bonsai:fix command runs in a transaction by default. For large trees, this may time out.php artisan bonsai:fix App\Models\Category --no-transaction
---
How can I help you explore Laravel packages today?