Weave 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

Tree Laravel Package

nicmart/tree

A flexible PHP tree data structure with a fluent builder API. Create nodes with any value, add or remove children, manage parents automatically, and build hierarchies easily for menus, categories, and nested data.

View on GitHub
Deep Wiki
Context7

Getting Started

nicmart/tree is a plain Composer library — no service provider, no config file, no auto-discovery. Install it and it is immediately usable anywhere in a Laravel app.

composer require nicmart/tree

Pin ^0.10 (current line supports PHP 8.4). Nothing to publish, nothing to register.

Where to look first: the README's three sections map 1:1 to the API surface you'll actually use:

  1. Tree\Node\Node — the data structure (getValue/setValue, addChild/removeChild/setChildren, getChildren, getParent, getAncestors, root, getDepth, getHeight, getSize, isLeaf/isRoot/isChild).
  2. Tree\Builder\NodeBuilder — fluent construction (value, leaf, tree, end, getNode).
  3. Tree\Visitor\{PreOrderVisitor, PostOrderVisitor, YieldVisitor} — traversal via $node->accept($visitor).

First use case — a nested menu from config:

use Tree\Builder\NodeBuilder;
use Tree\Visitor\PreOrderVisitor;

$builder = (new NodeBuilder())->value('root');

$builder->tree('Products')
    ->leaf('Phones')
    ->leaf('Laptops')
    ->end()
->leaf('About');

$root = $builder->getNode();

Then flatten it for rendering:

$rows = collect($root->accept(new PreOrderVisitor()))
    ->reject(fn ($node) => $node->isRoot())
    ->map(fn ($node) => [
        'label' => $node->getValue(),
        'depth' => $node->getDepth(),
    ]);

Return $rows to a Blade component, indent by depth, done. That's the 80% use case: derive a small tree from config/DB, flatten it once, render it.

Implementation Patterns

Hydrating from Eloquent (the dominant pattern). The package is in-memory only, so you own the parent_id table and the mapping. Build the whole tree in one pass from a single query — never query per node.

$categories = Category::query()->orderBy('position')->get(['id', 'parent_id', 'name']);

$nodes = $categories->mapWithKeys(
    fn (Category $c) => [$c->id => new Node($c)]
)->all();

foreach ($categories as $category) {
    $parent = $nodes[$category->parent_id] ?? null;
    if ($parent !== null) {
        $parent->addChild($nodes[$category->id]);
    }
}

$roots = array_values(array_filter($nodes, fn (Node $n) => $n->isRoot()));

Sort in SQL before attaching — child order is insertion order and nothing else. Return $nodes keyed by ID as your lookup index, and treat "node missing a parent" as an orphan you skip (or log) rather than a root.

Service class + caching. Trees are cheap to build, not free. Wrap hydration in a CategoryTree service and cache the flattened output, not the node graph (the graph doesn't serialize cleanly):

$menu = Cache::remember('menu.tree', now()->addHour(), fn () => $this->categoryTree->flatten());

Bust the cache in a Category observer's saved/deleted hooks.

Breadcrumbs from getAncestorsAndSelf(). Given a node for the current category:

$trail = collect($node->getAncestorsAndSelf())
    ->map(fn (Node $n) => $n->getValue())
    ->all();

Note getAncestors() excludes the node itself; the ...AndSelf variant is what you want for breadcrumbs.

Permission/role inheritance. Attach roles as node values in a hierarchy once, then $roleNode->getAncestors() is your effective-permission walk. Cache the resolved set per role.

Menu building from config with a recursive builder. The tree()/end() protocol maps naturally onto a recursive function; every tree() needs exactly one matching end():

function build(NodeBuilder $builder, array $items): void
{
    foreach ($items as $item) {
        if (empty($item['children'])) {
            $builder->leaf($item['label']);
            continue;
        }

        $builder->tree($item['label']);
        build($builder, $item['children']);
        $builder->end();
    }
}

For flat siblings, ->leafs('A', 'B', 'C') (variadic since 0.7.1) saves noise.

Subtree moves. Moving a category in the DB and moving it in the graph are two separate operations:

$oldParent->removeChild($node); // exact instance, not a look-alike
$newParent->addChild($node);    // resets $node's parent

removeChild() needs the identical object — identity, not value equality. Keep your $nodes map around if you're doing bulk moves.

Custom visitors for domain logic. Implement Tree\Visitor\Visitor when you need filtering or aggregation; the built-ins only do traversal and return arrays.

final class PublishedOnlyVisitor implements Visitor
{
    public function visit(NodeInterface $node): array
    {
        $out = $node->getValue()->isPublished() && ! $node->isRoot() ? [$node] : [];

        foreach ($node->getChildren() as $child) {
            $out = [...$out, ...$child->accept($this)];
        }

        return $out;
    }
}

Subclass when you need behavior on the node. Node is a thin class over NodeTrait; either extend it or apply the trait to your own class and satisfy NodeInterface. Handy for CategoryNode with url() or isVisible() helpers so Blade doesn't need to unpack values.

Gotchas and Tips

getNode() returns the current builder position, not the root. This is the single most common bug: forget an end() and your "root" is a nested subtree, and everything downstream silently renders a fragment. Every tree() must be balanced by an end(). Sanity-check $node->isRoot() before you hand the tree to a view.

Reuse one builder per tree, never across trees. NodeBuilder is stateful and mutable, and it has no reset(). new NodeBuilder() inside the factory method; don't inject a shared one.

getNode() on an empty builder throws LogicException (since 0.7.2). If a tree is optional (empty menu config, empty result set), short-circuit before calling it.

Parent pointers are managed by addChild(), but not by setChildren(). Overwriting the children array leaves the old children pointing at a parent that no longer lists them — isChild() lies, root() walks the wrong way. If you need setChildren(), call removeAllChildren() first or detach manually.

No cycle or duplicate protection. addChild() will happily attach a node already in the tree or create a loop, and getSize() / getHeight() / getDepth() are recursive — an accidental cycle means infinite recursion or a memory blowup. In hydrated-from-DB code this bites when parent_id data is corrupt; validate that the graph is acyclic (or trust a DB constraint) before traversal.

Identity, not value, semantics. Two new Node('Phones') are different nodes. removeChild(), getNeighbors() and friends compare by reference. Never search the tree by value and then try to remove based on the result unless it's the same instance.

getNeighbors() excludes self and returns [] for a root (both fixed/behaved as of 0.7.0). Use getNeighborsAndSelf() when you want the tri-state.

getDepth() and getHeight() both use edge counts. Root depth is 0; a leaf's height is 0. Assert these in a test before writing layout math against them — off-by-one indentation is easy to ship.

Visitors are eager and recursive. PreOrderVisitor, PostOrderVisitor and YieldVisitor build a full array of nodes (not values), and the traversal itself is recursive. On a 50k-node tree you'll pay for it in memory and recursion depth. For large datasets, aggregate in SQL (recursive CTE / materialized path) and use this package only for the presentation slice. PHP has no tail-call optimization, so deep degenerate chains are a real risk.

No serialization. There is no toArray()/toJson(). Write your own recursive serializer and map values yourself:

$payload = collect($root->accept(new PreOrderVisitor()))
    ->map(fn (Node $n) => ['id' => $n->getValue()->id, 'depth' => $n->getDepth()])
    ->all();

Don't put Eloquent models in node values unless you control loading. It's convenient (new Node($category)) and tempting in Blade ($node->getValue()->children), but it's an N+1 factory: the graph doesn't eager-load, and lazy loading inside a recursive view is the classic performance cliff. Either pass IDs and re-query once with with(), or eager-load everything before hydrating and access only loaded relations.

Arrays in, arrays out. No Collection returns anywhere. Wrap with collect() at the boundary; don't leak NodeInterface into Blade or API resources — flatten to plain arrays/DTOs in the service layer.

Strict types since 0.7. root(): static, getSize(): int, parameter types on addChild/removeChild. If you subclass Node, match the return types exactly or you'll get fatal signature errors.

No ordering or balance guarantees. Children come back in insertion order. If your UI needs alphabetic or positional ordering, sort upstream (SQL) — sorting after the fact means re-inserting children.

Debugging: there's no dump helper. Two lines cover most cases:

dump(collect($node->accept(new PreOrderVisitor()))->map->getValue()->all());
dump($node->getDepth(), $node->getHeight(), $node->getSize());

If getSize() disagrees with your row count, the usual cause is orphan rows silently dropped during hydration.

Extension points to reach for: NodeTrait (apply to your own class), Visitor (domain-specific traversal), BuilderInterface (if you generate trees from a different source than the fluent builder, e.g. an adjacency list). Everything else is final in spirit — the library is small and deliberately stable, so prefer composing around it over forking behavior.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator