yuriitatur/nested-data

Nested data storage-agnostic library

Maintainers

Package info

bitbucket.org/yurii_tatur/nested-data

pkg:composer/yuriitatur/nested-data

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

dev-master 2026-04-06 13:28 UTC

This package is auto-updated.

Last update: 2026-04-06 13:28:10 UTC


README

Quality Gate Status Coverage

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.