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. **Install the package**:
   ```bash
   composer require baril/bonsai
  1. Apply the trait to your Eloquent model:
    use Baril\Bonsai\Concerns\BelongsToTree;
    
    class Category extends Model
    {
        use BelongsToTree;
    
        // Customize if needed
        protected $parentForeignKey = 'parent_id';
        protected $closureTable = 'category_tree';
    }
    
  2. Generate the closure table migration:
    php artisan bonsai:grow App\Models\Category
    php artisan migrate
    
  3. Populate existing data (if applicable):
    php artisan bonsai:fix App\Models\Category
    

First Use Case: Creating a Tree Structure

// 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();

Implementation Patterns

Core Workflows

1. Tree Manipulation

// 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();

2. Querying the Tree

// Get all descendants (with depth)
$node->descendants()->withDepth()->get();

// Get ancestors ordered by depth
$node->ancestors()->orderByDepth()->get();

// Find common ancestor
$node->findCommonAncestorWith($otherNode);

3. Relationships

// Eager-load descendants (recursively)
$categories = Category::with('descendants')->get();

// Get siblings (excluding self)
$node->siblings()->get();

// Get siblings including self
$node->siblings()->withSelf()->get();

4. Scopes

// Query only roots
Category::onlyRoots()->get();

// Query leaves (nodes with no children)
Category::onlyLeaves()->get();

// Query nodes with children
Category::hasChildren()->get();

Integration Tips

Soft Deletes

use Baril\Bonsai\Concerns\SoftDeletes;

class Category extends Model
{
    use BelongsToTree, SoftDeletes;
}

// Restore a soft-deleted node and its descendants
$node->restoreTree();

Ordered Trees (with orderly)

use Baril\Bonsai\Concerns\Orderable;

class Category extends Model
{
    use BelongsToTree, Orderable;

    protected $orderColumn = 'position';
}

// Query ordered children
$node->children()->ordered()->get();

Customizing Table/Column Names

class Category extends Model
{
    use BelongsToTree;

    protected $parentForeignKey = 'parent_category_id';
    protected $closureTable = 'custom_category_tree';
}

Bulk Operations

// Move multiple nodes to a new parent
$nodes = Category::where('parent_id', $oldParent)->get();
foreach ($nodes as $node) {
    $node->graftOnto($newParent);
}

Gotchas and Tips

Pitfalls

  1. Circular References

    • Attempting to set a node as its own parent or a descendant will throw a TreeException.
    • Fix: Validate parent assignments before saving.
  2. Closure Table Sync

    • The closure table is not automatically synced on every save(). It updates only when the parent_id changes.
    • Tip: Use save() after modifying parent_id to ensure sync.
  3. Soft Deletes and Parentage

    • Restoring a soft-deleted node will fail if its original parent is missing.
    • Fix: Use cut() before restoring if the parent is gone:
      try {
          $node->restore();
      } catch (\Baril\Bonsai\TreeException $e) {
          $node->cut()->restore();
      }
      
  4. Performance with Deep Trees

    • Queries like descendants() or ancestors() can be slow for very deep trees.
    • Tip: Use maxDepth() to limit results:
      $node->descendants()->maxDepth(3)->get();
      
  5. Eager Loading Quirks

    • with('descendants') loads all descendants recursively by default. For large trees, this can be expensive.
    • Tip: Use with(['descendants' => function($query) { ... }]) to constrain the query.

Debugging Tips

  1. Inspect the Closure Table

    • Use bonsai:show to visualize the tree structure:
      php artisan bonsai:show App\Models\Category --label=name --depth=5
      
  2. Check for Orphaned Closures

    • If the tree behaves unexpectedly, run bonsai:fix to rebuild the closure table:
      php artisan bonsai:fix App\Models\Category
      
  3. Enable Query Logging

    • Debug complex queries with:
      \DB::enableQueryLog();
      $node->descendants()->get();
      dd(\DB::getQueryLog());
      

Extension Points

  1. Custom Scopes

    • Extend the trait to add domain-specific scopes:
      class Category extends Model
      {
          use BelongsToTree;
      
          public function scopeActive($query)
          {
              return $query->where('is_active', true);
          }
      }
      
  2. Override Tree Methods

    • Extend functionality by overriding methods (e.g., getDepth()):
      public function getDepth()
      {
          $depth = parent::getDepth();
          return $depth + 1; // Custom logic
      }
      
  3. Add Custom Closure Columns

    • Extend the closure table migration to include extra fields:
      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']);
      });
      
  4. Event Listeners

    • Hook into tree events (e.g., saved, deleted) to trigger side effects:
      $node->saved(function ($node) {
          // Post-save logic
      });
      

Configuration Quirks

  1. UUID Primary Keys

    • The package supports UUIDs out of the box (no extra config needed).
  2. Multi-Database Setups

    • Ensure the closure table is created in the correct database if using multiple connections.
    • Tip: Specify the connection in the bonsai:grow command:
      php artisan bonsai:grow App\Models\Category --connection=secondary
      
  3. Case-Sensitive Table Names

    • On some databases (e.g., PostgreSQL), table names are case-sensitive. Use consistent casing in $closureTable.
  4. Transaction Handling

    • The bonsai:fix command runs in a transaction by default. For large trees, this may time out.
    • Tip: Disable transactions for large fixes:
      php artisan bonsai:fix App\Models\Category --no-transaction
      

---
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.
nexmo/api-specification
capell-app/block-library
axium/identity
cetria/laravel-dummy-models
cetria/reflection-helper
agropredict/sso-auth-bundle
evolvestudio/spam-protection
datacore/hub-sdk
develia/commons
cuci/prototurk-sdk
cuci/prototurk-sdk-symfony
develia/geo-bundle
dreamzy/livewire-charts
touchestate-sdk/php-sdk
ecotone/kafka
22h/doctrine-garbage-collection-bundle
agtp/agtp-php
agtp/mod-php
splash/sonata-admin
splash/metadata