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

bluem/tree

bluem/tree is a PHP library for building and manipulating hierarchical tree structures from flat, parent-child data. It offers node traversal, filtering, and rendering helpers, making it useful for categories, menus, and other nested data in Laravel or plain PHP projects.

Deep Wiki
Context7

Getting Started

Install with Composer:

composer require bluem/tree

Look first at:

  • The README usage section for constructor options and traversal methods.
  • src/Tree.php for getRootNodes(), getNodeById(), getChildren(), getDescendants(), getPath(), etc.
  • src/Tree/Node.php for node-level traversal and access to the original row data.
  • src/Tree/Adapter/ if you want to feed the tree directly from PDO or another source.

This package is best treated as an in-memory read model for a flat, self-joined table: load rows once, build a Tree, then traverse it. It is not an ORM and it does not manage writes or schema changes.

First use case: a category/menu tree from a categories table with id, parent_id, and name.

use BlueM\Tree;

$rows = $pdo->query(
    'SELECT id, parent_id AS parent, name FROM categories ORDER BY name'
)->fetchAll(PDO::FETCH_ASSOC);

$tree = new Tree($rows, [
    'id' => 'id',
    'parent' => 'parent',
    'root' => 0, // default sentinel; adjust for NULL roots
]);

$node = $tree->getNodeById(42);
$children = $node->getChildren();
$path = $node->getPath(); // usually root -> ... -> current node
$level = $node->getLevel();

A minimal recursive renderer:

function renderNode(BlueM\Tree\Node $node, int $depth = 0): void
{
    $label = $node->getData()['name'] ?? $node->getId();
    echo str_repeat('  ', $depth) . $label . PHP_EOL;

    foreach ($node->getChildren() as $child) {
        renderNode($child, $depth + 1);
    }
}

foreach ($tree->getRootNodes() as $root) {
    renderNode($root);
}

If your database uses NULL for root parents, normalize rows before building or set the root option to null consistently.

Implementation Patterns

Load once, traverse many times. The common pattern is to fetch all rows in one query, build the Tree once per request, and use it for menus, breadcrumbs, permission checks, and API output. Avoid lazy-loading children from the database per node; that defeats the purpose of the package.

Use it as a read-model service. In Laravel, put tree construction in a repository, action, or service class:

final class CategoryTree
{
    public function __construct(private PDO $pdo) {}

    public function build(): Tree
    {
        $rows = $this->pdo->query(
            'SELECT id, parent_id AS parent, name FROM categories ORDER BY name'
        )->fetchAll(PDO::FETCH_ASSOC);

        return new Tree($rows, [
            'id' => 'id',
            'parent' => 'parent',
            'root' => 0,
        ]);
    }
}

Cache the raw rows, not necessarily the built object:

$rows = Cache::remember('category_rows', 3600, fn () => DB::table('categories')
    ->select('id', 'parent_id as parent', 'name')
    ->orderBy('name')
    ->get()
    ->map(fn ($row) => (array) $row)
    ->all());

Then rebuild the Tree cheaply from cached rows.

Breadcrumbs and ancestry. Use getPath() for a root-to-node breadcrumb and getAncestors() when you need the parent chain without the current node. Add the current node explicitly if your UI expects it.

$crumbs = array_map(
    fn ($node) => $node->getData()['name'],
    $tree->getNodeById($categoryId)->getPath()
);

Subtree extraction. Use getDescendants() to collect a subtree below a node. If you need the selected node included, add it yourself. This is useful for scoped menus, category filters, and bulk operations.

$node = $tree->getNodeById($categoryId);
$ids = array_map(fn ($n) => $n->getId(), $node->getDescendants());
$ids[] = $node->getId(); // include the current node if needed

Moving nodes. The library reads parent IDs; it does not persist changes. For drag-and-drop or “move category” features, validate the new parent first, update parent_id in the database, then rebuild the tree. Validate that the new parent is not the node itself and not one of its descendants to avoid cycles.

Sorting. Sorting in SQL is usually the most predictable approach because the final order is visible in the query. If you use the package’s sort options, verify option names and comparison behavior against your installed version. For stable menus, always include a tie-breaker such as ORDER BY sort_order, name, id.

Serialization for APIs. Do not return Node objects directly from controllers if you can avoid it. Map the tree to plain arrays with only the fields the client needs. This avoids accidental exposure of internal references and makes JSON output stable.

function nodeToArray(BlueM\Tree\Node $node): array
{
    return [
        'id' => $node->getId(),
        'name' => $node->getData()['name'] ?? null,
        'children' => array_map('nodeToArray', $node->getChildren()),
    ];
}

Custom adapters. If your schema is legacy or unusual, implement the package adapter interface and return rows with the configured id and parent keys. Keep the adapter thin: it should fetch flat rows, not perform tree logic.

Testing. Build small fixture arrays in tests rather than hitting the database. Assert root nodes, child order, path contents, descendant IDs, and level values. This catches version-specific differences in inclusion semantics early.

Gotchas and Tips

  • Root sentinel mismatch. The package often defaults root to 0, while many schemas use NULL. Normalize NULL to 0 or set the root option correctly. Mixed 0 and NULL roots cause nodes to disappear or become unexpected roots.

  • Orphaned parents. If a row points to a missing parent, the node may be dropped, treated as a root, or trigger an exception depending on version and configuration. Validate before building:

    SELECT c.id
    FROM categories c
    LEFT JOIN categories p ON p.id = c.parent_id
    WHERE c.parent_id IS NOT NULL AND p.id IS NULL;
    
  • Duplicate IDs. Node IDs must be unique. Duplicate IDs can overwrite nodes or produce confusing traversal results.

  • Type juggling. MySQL often returns IDs as strings. Keep ID and parent types consistent before building the tree. Mixed "1" and 1 can break parent matching in strict comparisons.

  • Cycles. A parent chain must be acyclic. The library is not a write-side guard. Detect cycles before saving a new parent, especially for drag-and-drop interfaces.

  • Inclusion semantics. Verify whether getPath(), getAncestors(), and getDescendants() include the current node in your installed version. Write a small test rather than assuming.

  • Memory limits. The whole tree is built in memory. It is fine for hundreds or low thousands of nodes; very large trees should use a database-native approach such as recursive CTEs, closure tables, or nested sets.

  • Do not cache Node graphs blindly. Nodes reference the tree and each other. Serializing them can produce circular references or large payloads. Cache the flat rows and rebuild.

  • Tree is a snapshot. Updating parent_id in the database does not update an existing Tree instance. Rebuild after writes.

  • Guard getNodeById(). Depending on version and input, a missing ID may return null or throw. Handle both possibilities at the boundary.

  • Sorting quirks. Database collation may differ from PHP string comparison. If menu order matters, sort in SQL and verify the output in tests.

  • Deep recursion. Recursive rendering is fine for normal menus, but very deep trees can exhaust the stack. Use an iterative stack if depth is unbounded.

  • Read the source for edge cases. This is a small, focused package. The README is helpful, but for adapter constructors, sort options, and exception behavior, the source is the final authority.

  • Extension points. Use a custom adapter for unusual schemas, and decorate or map nodes by ID for computed fields. Keep package Node objects as traversal objects and map them to your own DTOs at the application boundary.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource