Weave Code
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

Bonsai Laravel Package

baril/bonsai

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation

    composer require baril/bonsai
    

    Ensure your Laravel version matches the compatibility table.

  2. Publish Migrations

    php artisan vendor:publish --provider="Baril\Bonsai\BonsaiServiceProvider" --tag="migrations"
    

    Run the migrations:

    php artisan migrate
    
  3. Define a Tree Model Extend Baril\Bonsai\Tree and add tree() to your model:

    use Baril\Bonsai\Tree;
    
    class Category extends Tree
    {
        protected $treeType = 'categories'; // Optional: for multi-tree support
        public function tree()
        {
            return $this->belongsToMany(
                static::class,
                'categories_closure',
                'ancestor',
                'descendant',
                'id',
                'id'
            )->withPivot(['depth']);
        }
    }
    
  4. First Use Case: Create a Root Node

    $root = Category::create(['name' => 'Electronics']);
    $root->save(); // Automatically sets as root
    

Implementation Patterns

Core Workflows

1. Tree Creation & Manipulation

  • Add a Child
    $parent = Category::find(1);
    $child = $parent->children()->create(['name' => 'Phones']);
    
  • Reorder Nodes
    $node = Category::find(2);
    $node->moveTo($parent, 'after'); // or 'before', 'first', 'last'
    

2. Querying the Tree

  • Get Descendants (Subtree)
    $parent = Category::find(1);
    $descendants = $parent->descendants; // Includes all children, grandchildren, etc.
    
  • Get Ancestors (Path to Root)
    $node = Category::find(3);
    $ancestors = $node->ancestors; // Array of parent nodes up to root
    
  • Find by Path
    $node = Category::findByPath([1, 2, 3]); // Finds node with path [1 → 2 → 3]
    

3. Batch Operations

  • Detach a Subtree
    $subtree = Category::find(5);
    $subtree->detach(); // Removes all descendants
    
  • Reattach a Subtree
    $subtree->attachTo($newParent);
    

4. Customizing Tree Behavior

  • Override Default Methods
    class Category extends Tree
    {
        public function getChildrenAttribute()
        {
            return $this->children()->where('is_active', true)->get();
        }
    }
    
  • Multi-Tree Support Use $treeType to manage separate trees (e.g., categories, tags).

Integration Tips

1. Scopes for Common Queries

Add custom scopes to your model:

class Category extends Tree
{
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeWithChildren($query)
    {
        return $query->with('children');
    }
}

Usage:

$activeCategories = Category::active()->withChildren()->get();

2. Caching for Performance

Cache frequent tree queries:

$root = Category::find(1);
Cache::remember("category_{$root->id}_subtree", now()->addHours(1), function () use ($root) {
    return $root->descendants;
});

3. API Responses

Serialize trees for APIs:

public function toArray()
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'children' => $this->children->map(fn ($child) => $child->toArray()),
    ];
}

4. Events & Observers

Listen for tree changes:

class CategoryObserver
{
    public function saved(Tree $model)
    {
        if ($model->isRoot()) {
            event(new CategoryRootCreated($model));
        }
    }
}

Register in AppServiceProvider:

Category::observe(CategoryObserver::class);

Gotchas and Tips

Pitfalls

1. Migration Conflicts

  • If you manually altered the closure table, drop and re-run migrations to avoid schema mismatches.
  • Multi-tree support requires unique tree_type values; collisions will break queries.

2. Performance with Deep Trees

  • Avoid descendants on very deep trees (e.g., >100 levels). Use pagination:
    $descendants = $node->descendants()->paginate(50);
    
  • Index the closure table on (ancestor, descendant) for faster joins.

3. Circular References

  • Detaching a node while iterating its descendants will throw errors. Use:
    $node->detach(); // Safe: no active queries
    
  • Avoid recursive loops when moving nodes:
    $node->moveTo($node); // Throws exception (prevents self-referential loops)
    

4. Depth Calculation

  • Depth is not automatically updated on manual save(). Use:
    $node->saveWithDepth(); // Explicitly recalculates depth
    
  • For bulk updates, use:
    Category::recalculateDepths();
    

Debugging

1. Query Logs

Enable query logging to inspect generated SQL:

DB::enableQueryLog();
$node->descendants; // Trigger query
dd(DB::getQueryLog());

2. Common SQL Issues

  • Missing joins: Ensure tree() defines the correct pivot table.
  • Wrong depth: Verify depth column exists in the pivot table.
  • Slow queries: Add indexes to ancestor/descendant columns.

3. Testing Trees

Use BonsaiTestCase for assertions:

use Baril\Bonsai\Testing\BonsaiTestCase;

class CategoryTest extends BonsaiTestCase
{
    public function testTreeStructure()
    {
        $root = Category::create(['name' => 'Root']);
        $child = $root->children()->create(['name' => 'Child']);

        $this->assertCount(1, $root->children);
        $this->assertEquals(1, $child->depth);
    }
}

Extension Points

1. Custom Closure Table

Override the default closure table name:

protected $closureTable = 'custom_closure_table';

2. Custom Depth Calculation

Extend Baril\Bonsai\Tree to modify depth logic:

protected function calculateDepth()
{
    // Custom logic (e.g., skip certain nodes)
    return parent::calculateDepth() + 1;
}

3. Soft Deletes

Enable soft deletes for trees:

use Illuminate\Database\Eloquent\SoftDeletes;

class Category extends Tree
{
    use SoftDeletes;
    protected $dates = ['deleted_at'];
}

Note: Soft-deleted nodes are not included in descendants/ancestors by default. Use:

$node->withTrashed()->descendants;

4. Custom Pivot Data

Add extra pivot columns to the closure table:

public function tree()
{
    return $this->belongsToMany(
        static::class,
        'categories_closure',
        'ancestor',
        'descendant',
        'id',
        'id'
    )->withPivot(['depth', 'custom_metadata']);
}

Access via:

$node->children->first()->pivot->custom_metadata;

5. Multi-Database Support

For multi-database setups, specify the connection:

public function tree()
{
    return $this->belongsToMany(
        static::class,
        'categories_closure',
        'ancestor',
        'descendant',
        'id',
        'id'
    )->setConnection('secondary');
}
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.
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope