Installation:
composer require jiaxincui/closure-table
Publish the migration (if needed):
php artisan vendor:publish --provider="Jiaxincui\ClosureTable\ClosureTableServiceProvider" --tag=migrations
Define a Tree Model:
Extend Jiaxincui\ClosureTable\Tree and implement getTreeTable():
use Jiaxincui\ClosureTable\Tree;
class Category extends Tree
{
protected $treeTable = 'categories'; // Your table name
protected $parentKey = 'parent_id'; // Foreign key to parent
protected $selfKey = 'id'; // Self-referencing key
}
Run Migrations:
php artisan migrate
First Use Case:
// Create a root node
$root = Category::create(['name' => 'Root']);
// Create a child node
$child = $root->children()->create(['name' => 'Child']);
// Fetch the tree structure
$tree = $root->withDescendants()->get();
Hierarchy Management:
// Add a child to a node
$parent->children()->create(['name' => 'New Child']);
// Move a node (reparent)
$node->moveTo($newParent);
// Reorder siblings
$node->moveBefore($sibling);
Querying:
// Get all descendants (recursive)
$descendants = $node->descendants;
// Get ancestors (recursive)
$ancestors = $node->ancestors;
// Get siblings
$siblings = $node->siblings;
// Filter descendants by condition
$activeChildren = $node->descendants()->where('is_active', true)->get();
Tree Traversal:
// Depth-first traversal (Laravel 8+)
$node->eachDescendant(function ($descendant) {
// Process each descendant
});
// Breadcrumbs
$breadcrumbs = $node->getBreadcrumbs();
Performance Optimization:
// Eager-load descendants (avoid N+1)
$node->loadDescendants();
// Use `withDepth()` for depth-aware queries
$tree = Category::withDepth()->get();
Custom Closure Table:
Override getTreeTable() and getClosureTable() to use custom tables:
protected $closureTable = 'category_closure';
Soft Deletes:
Extend Jiaxincui\ClosureTable\SoftDeletes for soft-deletable trees:
use Jiaxincui\ClosureTable\SoftDeletes;
class Category extends Tree
{
use SoftDeletes;
}
Scopes: Add custom scopes for filtering:
public function scopeActive($query)
{
return $query->where('is_active', true);
}
API Responses:
Serialize trees with toTreeArray():
return response()->json($node->toTreeArray());
Closure Table Sync:
moveTo(), moveBefore(), or moveAfter() instead of direct DB updates.php artisan db:seed to reset test data if closures are corrupted.Performance:
withDepth() and limit recursion in queries:
$shallowTree = Category::withDepth()->whereDepth('<', 5)->get();
Circular References:
if ($node->isAncestorOf($newParent)) {
throw new \Exception("Cannot create circular reference!");
}
Migration Conflicts:
closure_table migrations may clash.php artisan vendor:publish --tag=migrations
Inspect Closure Table:
SELECT * FROM categories_closure WHERE ancestor = 1;
(Replace 1 with your node ID.)
Log Tree Structure:
$node->eachDescendant(function ($descendant) {
\Log::debug("Node {$descendant->id}: {$descendant->name}");
});
Check for Orphaned Entries:
// Find nodes with no closure entries
$orphans = Category::doesntHave('closureEntries')->get();
Custom Closure Logic:
Override syncClosureTable() to add business logic:
protected function syncClosureTable()
{
// Custom logic before sync
parent::syncClosureTable();
// Custom logic after sync
}
Event Hooks: Listen for tree events:
Category::moved(function ($node, $oldParent, $newParent) {
// Trigger analytics, notifications, etc.
});
Custom Tree Methods: Add helper methods to your model:
public function getPath()
{
return $this->ancestors->pluck('name')->implode(' > ') . ' > ' . $this->name;
}
Multi-Tenant Trees: Use model events to scope trees per tenant:
protected static function boot()
{
static::addGlobalScope('tenant', function (Builder $builder) {
$builder->where('tenant_id', auth()->id());
});
}
How can I help you explore Laravel packages today?