cline/ancestry
Closure table hierarchies for Laravel Eloquent. Manage deep trees (org charts, categories) with O(1) ancestor/descendant queries, fluent APIs, configurable keys/types, events, and snapshots for point-in-time hierarchy state.
Installation:
composer require cline/ancestry
Add the trait to your Eloquent model:
use Ancestry\AncestryTrait;
class Category extends Model
{
use AncestryTrait;
}
Migration:
Add ancestry column to your table:
Schema::table('categories', function (Blueprint $table) {
$table->string('ancestry')->nullable();
});
First Use Case:
// Create a root node
$root = Category::create(['name' => 'Root']);
// Create a child
$child = $root->children()->create(['name' => 'Child']);
// Query descendants
$descendants = $root->descendants;
Hierarchy Creation:
// Add child to existing node
$parent = Category::find(1);
$child = $parent->children()->create(['name' => 'New Child']);
// Or create with ancestry string
$node = Category::create(['name' => 'New Node', 'ancestry' => '1,2']);
Querying:
// Get all descendants (O(1) operation)
$root->descendants;
// Get all ancestors
$node->ancestors;
// Check if node is descendant of another
$node->isDescendantOf($root);
// Get siblings
$node->siblings;
Reorganizing:
// Move node to new parent
$node->moveTo($newParent);
// Make node a root (detach from hierarchy)
$node->makeRoot();
Scopes: Extend the trait with custom scopes:
public function scopeActive($query)
{
return $query->where('active', true)->whereHasAncestor(function ($q) {
$q->where('active', true);
});
}
Events: Listen for hierarchy changes:
Category::created(function ($model) {
if ($model->ancestry) {
event(new HierarchyChanged($model));
}
});
API Responses: Serialize hierarchies efficiently:
$response->json($node->with(['children' => function ($query) {
return $query->orderBy('name');
}]));
Caching: Cache frequent queries (e.g., root nodes):
Cache::remember('root_categories', now()->addHours(1), function () {
return Category::roots()->get();
});
Circular References:
if ($node->isDescendantOf($newParent)) {
throw new \InvalidArgumentException('Cannot move node to its descendant');
}
Performance with Large Trees:
ancestry_depth column (add via migration) and limit depth in queries:
$query->where('ancestry_depth', '<', 5);
Race Conditions:
DB::transaction(function () use ($node, $newParent) {
$node->moveTo($newParent);
});
Serialization:
protected $casts = [
'ancestry' => 'hidden',
];
Validate Ancestry Strings:
if (!$model->ancestry || !Ancestry::isValid($model->ancestry)) {
throw new \InvalidArgumentException('Invalid ancestry string');
}
Log Hierarchy Changes:
Category::updated(function ($model) {
if ($model->isDirty('ancestry')) {
\Log::debug('Ancestry changed', ['model' => $model->id, 'new_ancestry' => $model->ancestry]);
}
});
Custom Ancestry Logic:
Override getAncestryAttribute or setAncestryAttribute in your model:
public function getAncestryAttribute($value)
{
return $value ? explode(',', $value) : [];
}
Add Metadata: Extend the trait to store additional hierarchy data:
protected $ancestryMetadata = [];
public function setAncestryMetadata($key, $value)
{
$this->ancestryMetadata[$key] = $value;
$this->save();
}
Custom Query Builder: Extend the query builder for hierarchy-specific methods:
public function scopePathTo($query, $ancestor)
{
return $query->where(function ($q) use ($ancestor) {
$q->where('ancestry', 'LIKE', $ancestor->ancestry . ',%')
->orWhere('id', $ancestor->id);
});
}
Ancestry Column Type:
string (255 chars). For deeper trees, consider text.$table->text('ancestry')->nullable();
Case Sensitivity:
$model->ancestry = strtolower($model->ancestry);
Empty Ancestry:
null ancestry. Ensure your queries handle this:
$roots = Category::whereNull('ancestry')->get();
Indexing: Add database indexes for faster queries:
Schema::table('categories', function (Blueprint $table) {
$table->index('ancestry');
});
Materialized Path Alternative:
For read-heavy workloads, consider storing materialized paths (e.g., left/right columns) alongside ancestry for faster queries.
Batch Operations: Use chunking for bulk hierarchy updates:
Category::chunk(200, function ($categories) {
foreach ($categories as $category) {
$category->moveTo($newParent);
}
});
How can I help you explore Laravel packages today?