yuriitatur / nested-data
Nested data storage-agnostic library
Requires
- php: >=8.4
- yuriitatur/repository: dev-master
Requires (Dev)
- dg/bypass-finals: ^1.9
- kint-php/kint: ^6.0
- orchestra/testbench: ^10.6
- phpunit/phpunit: ^12.3
- yuriitatur/repository-laravel: dev-master
This package is auto-updated.
Last update: 2026-04-06 13:28:10 UTC
README
Nested Data
A storage-agnostic PHP 8.4+ library for managing hierarchical data (categories, menus, organizational trees) using a materialized path model. Provides a repository decorator that wraps any RepositoryInterface-compatible repository and adds hierarchy querying capabilities.
Installation
composer require yuriitatur/nested-data
Setup
1. Prepare your entity
Implement NestedEntity and use NestedEntityTrait in your domain entity:
use YuriiTatur\Nested\Entities\NestedEntity;
use YuriiTatur\Nested\Entities\NestedEntityTrait;
class Category implements NestedEntity
{
use NestedEntityTrait;
public function __construct(
private readonly int $id,
public string $name,
) {}
public function getId(): int
{
return $this->id;
}
}
2. Create the database table (Laravel)
Add a dedicated hierarchy table in a migration:
use YuriiTatur\Nested\Laravel\Database\NestedTable;
Schema::create('nested', function (Blueprint $table) {
$table->id();
NestedTable::register($table); // adds entity_id, path, depth columns
});
The entity_id column name can be customised:
NestedTable::register($table, 'category_id');
3. Wrap your repository
use YuriiTatur\Nested\Repositories\HierarchyRepository;
use YuriiTatur\Nested\Laravel\Drivers\EloquentHierarchyDriver;
$driver = new EloquentHierarchyDriver(
table: 'nested',
primaryKey: 'id',
relatedEntityId: 'entity_id',
);
$repo = new HierarchyRepository(
innerRepository: $categoryRepository, // your existing RepositoryInterface
hierarchyDatabaseDriver: $driver,
transactions: $transactionRunner,
ancestryDataHydrator: $hydrator,
events: $eventDispatcher,
);
Managing hierarchy
Adding a node to the tree
use YuriiTatur\Nested\ValueObjects\ParentPath;
// Place $category as a root node
$repo->addParentPath($category, new ParentPath());
// Place $category as a child of node with ID 1
$repo->addParentPath($category, new ParentPath(1));
// Place $category as a grandchild: parent chain 1 → 3
$repo->addParentPath($category, new ParentPath(1, 3));
addParentPath validates that:
- The parent path already exists in the hierarchy.
- The path does not create a circular dependency.
Removing a node from a position
$repo->deleteParentPath($category, new ParentPath(1, 3));
Deleting an entity entirely
Deleting an entity automatically removes all its ancestry records:
$repo->delete($category);
Querying
All query methods return a new immutable repository instance and can be chained with regular RepositoryInterface methods (where, orderBy, paginate, etc.).
Roots and depth
// All root nodes (depth = 0)
$roots = $repo->onlyRoots()->get();
// All nodes at depth 2
$depth2 = $repo->atDepthOf(2)->get();
Traversing the tree
// All ancestors of $category (every node in its path)
$ancestors = $repo->allAncestorsOf($category)->get();
// Only the direct parent
$parent = $repo->directAncestorOf($category)->get();
// All descendants at any depth
$subtree = $repo->allDescendantsOf($category)->get();
// Only direct children
$children = $repo->directDescendantsOf($category)->get();
// Siblings (same parent, different node)
$siblings = $repo->siblingsOf($category)->get();
Eager-loading descendants
By default descendants are lazy-loaded when $entity->getDescendants() is accessed. Use withDescendants() to load them in a single pass:
$categories = $repo->withDescendants()->onlyRoots()->get();
foreach ($categories as $category) {
// already loaded, no extra query
foreach ($category->getDescendants() as $child) { ... }
}
Getting entity IDs including the subtree
// Returns a Collection of IDs: the entity itself + all descendants
$ids = $category->getSubtreeIds();
Path value objects
ParentPath
Represents a single position in the hierarchy as an ordered list of ancestor IDs.
$path = new ParentPath(1, 3, 5); // represents /1/3/5/
(string) $path; // "/1/3/5/"
$path->depth; // 3
$path->getDirectParent(); // 5
$path->getParentsParents(); // [1, 3]
$path->hasParent(3); // true
// Reconstruct from a stored string
$path = ParentPath::fromStringPath('/1/3/5/');
PathList
A collection of ParentPath objects held on an entity (an entity can exist at multiple positions in the tree).
$list = $category->getPathList();
$list->hasPath(new ParentPath(1, 3)); // bool
Testing
composer test
License
This code is under MIT license, read more in the LICENSE file.