Skip to content

Stages

A stage wraps the node stream and changes it. Stages run in the order you add them. Filters are stages too.

Tree method Stage class Effect Memory
filter($predicate, $strategy) Filter Applies a strategy see the strategy
activeTrail(…), annotate(…) Filter Shortcuts for ActiveTrail and Annotate see the strategy
map($fn) Map Transforms each payload O(1)
maxDepth($max) MaxDepth Cuts everything deeper than $max O(1)
depthWindow($min, $max) DepthWindow Keeps depths $min..$max; nodes at $min become roots O(1)
limit($count) Limit Stops after $count nodes O(1)
sortSiblings($compare) SortSiblings Reorders the children of every node O(n)
pipe($stage) any Stage Adds your own stage —

The examples on this page use a small shop category tree from examples/support/shop.php:

Shop
├─ Clothing
│  ├─ Men            4 products
│  │  ├─ Shirts     12 products
│  │  └─ Shoes
│  └─ Women
│     └─ Dresses
├─ Electronics
│  ├─ Phones
│  │  └─ Cases      30 products
│  └─ Laptops        7 products
└─ Garden
   └─ Tools

map

Transforms each node: fn(mixed $node, int $depth): mixed. The structure stays the same. Children are attached later by the output, so return the node without children.

use Ghispi\TreeTransform\Tree;

require __DIR__ . '/../vendor/autoload.php';

$rows = require __DIR__ . '/support/shop.php';

$calls = 0;
$menu = Tree::fromNestedSet($rows)
    ->maxDepth(1)
    ->map(function (array $category, int $depth) use (&$calls): array {
        ++$calls;

        return ['label' => $category['title'], 'url' => '/c/' . $category['slug'], 'level' => $depth];
    })
    ->toNestedArray(omitEmptyChildren: true);

print_r($menu);
echo "map() called {$calls} times for ", count($rows), " rows\n";

Output:

Array
(
    [0] => Array
        (
            [label] => Shop
            [url] => /c/shop
            [level] => 0
            [children] => Array
                (
                    [0] => Array
                        (
                            [label] => Clothing
                            [url] => /c/clothing
                            [level] => 1
                        )

                    [1] => Array
                        (
                            [label] => Electronics
                            [url] => /c/electronics
                            [level] => 1
                        )

                    [2] => Array
                        (
                            [label] => Garden
                            [url] => /c/garden
                            [level] => 1
                        )

                )

        )

)
map() called 4 times for 13 rows

Source: examples/stages-map.php

Put map() after filters and depth limits, so it only runs for nodes that are kept: here 4 calls for 13 rows. Filters after a map() see the mapped nodes.

maxDepth

Cuts everything deeper than $max. Roots are depth 0, so maxDepth(0) keeps only the roots.

use Ghispi\TreeTransform\Tree;

require __DIR__ . '/../vendor/autoload.php';

$rows = require __DIR__ . '/support/shop.php';
$printTree = require __DIR__ . '/support/print-tree.php';

// Roots are depth 0: maxDepth(1) keeps the root and its children.
echo $printTree(Tree::fromNestedSet($rows)->maxDepth(1), fn(array $c) => $c['title']);

echo "\nmaxDepth(0):\n";
echo $printTree(Tree::fromNestedSet($rows)->maxDepth(0), fn(array $c) => $c['title']);

Output:

Shop
├─ Clothing
├─ Electronics
└─ Garden

maxDepth(0):
Shop

Source: examples/stages-max-depth.php

The cut subtrees are skipped by the source (skipSubtree()), so their rows are not tested by later filters or mapped. $max must be ≥ 0.

depthWindow

Keeps depths $min to $max, both inclusive. Nodes at $min become roots; everything below $max is cut. $max defaults to no limit. Use it to drop a site root, or to show "levels 2–3".

use Ghispi\TreeTransform\Tree;

require __DIR__ . '/../vendor/autoload.php';

$rows = require __DIR__ . '/support/shop.php';
$printTree = require __DIR__ . '/support/print-tree.php';

// Levels 1..2: the shop root disappears, its children become roots, grandchildren of them are cut.
echo $printTree(Tree::fromNestedSet($rows)->depthWindow(1, 2), fn(array $c) => $c['title']);

echo "\ndepthWindow(2) (no upper limit):\n";
echo $printTree(Tree::fromNestedSet($rows)->depthWindow(2), fn(array $c) => $c['title']);

Output:

Clothing
├─ Men
└─ Women
Electronics
├─ Phones
└─ Laptops
Garden
└─ Tools

depthWindow(2) (no upper limit):
Men
├─ Shirts
└─ Shoes
Women
└─ Dresses
Phones
└─ Cases
Laptops
Tools

Source: examples/stages-depth-window.php

Nodes above $min are read but not returned; nodes below $max are skipped by the source. depthWindow(0, $n) is the same as maxDepth($n). See also Recipes → Re-rooting.

limit

Stops after $count nodes in pre-order, so the result is still a valid tree. It stops reading the input too: with a database cursor, the remaining rows are never fetched.

use Ghispi\TreeTransform\Tree;

require __DIR__ . '/../vendor/autoload.php';

$printTree = require __DIR__ . '/support/print-tree.php';

// A generator stands in for a database cursor; it reports how many rows were fetched.
$fetched = 0;
$rows = (function () use (&$fetched): Generator {
    foreach (require __DIR__ . '/support/shop.php' as $row) {
        ++$fetched;
        yield $row;
    }
})();

echo $printTree(Tree::fromNestedSet($rows)->limit(4), fn(array $c) => $c['title']);
echo "Rows fetched: {$fetched} of 13\n";

Output:

Shop
└─ Clothing
   └─ Men
      └─ Shirts
Rows fetched: 4 of 13

Source: examples/stages-limit.php

Pre-order means limit() goes deep before it goes wide. For "the first 3 children of every node", write a custom stage (Extending → Stage).

sortSiblings

Reorders the children of every node, and the roots, with a comparator fn(mixed $a, mixed $b): int. Subtrees move with their root. Sorting is stable.

use Ghispi\TreeTransform\Tree;

require __DIR__ . '/../vendor/autoload.php';

$rows = require __DIR__ . '/support/shop.php';
$printTree = require __DIR__ . '/support/print-tree.php';

// Alphabetical within every parent; subtrees move with their root.
$sorted = Tree::fromNestedSet($rows)
    ->sortSiblings(fn(array $a, array $b) => strcmp($a['title'], $b['title']));

echo $printTree($sorted, fn(array $c) => $c['title']);

Output:

Shop
├─ Clothing
│  ├─ Men
│  │  ├─ Shirts
│  │  └─ Shoes
│  └─ Women
│     └─ Dresses
├─ Electronics
│  ├─ Laptops
│  └─ Phones
│     └─ Cases
└─ Garden
   └─ Tools

Source: examples/stages-sort-siblings.php

sortSiblings() reads the whole input before it returns the first node: O(n) memory. Sort in the database (ORDER BY for adjacency lists, or siblingOrder in fromAdjacencyList()) when you can. Put it after filters, so fewer nodes are buffered.

Chaining stages

Stages compose freely. Each filter() is its own stage with its own strategy, and runs on the output of the previous ones.

use Ghispi\TreeTransform\Strategy\KeepAncestors;
use Ghispi\TreeTransform\Tree;

require __DIR__ . '/../vendor/autoload.php';

$rows = require __DIR__ . '/support/shop.php';
$printTree = require __DIR__ . '/support/print-tree.php';

// Stages run in the order they are added. Each filter() is its own stage with its own strategy.
$base = Tree::fromNestedSet($rows)
    ->filter(fn(array $c) => $c['slug'] !== 'garden'); // hide a whole section (Prune)

$search = $base
    ->filter(fn(array $c) => str_contains(strtolower($c['title']), 'es'), new KeepAncestors())
    ->depthWindow(1);

echo $printTree($search, fn(array $c) => $c['title']);

// Tree is immutable: $base is unchanged and can be reused.
echo "\n", $base->count(), " categories without Garden\n";

Output:

Clothing
├─ Men
│  └─ Shoes
└─ Women
   └─ Dresses
Electronics
└─ Phones
   └─ Cases

11 categories without Garden

Source: examples/stages-chaining.php

Every method returns a new Tree, so a configured tree can be reused as a template.

Add your own stage with pipe(); see Extending → Stage.