ghispi / tree-transform
Efficient, streaming filtering and transformation of trees (nested set, adjacency list, materialized path, nested arrays) with pluggable strategies. Works with Gedmo Doctrine tree behaviours.
Requires
- php: >=8.4
Requires (Dev)
- doctrine/orm: ^3.6
- friendsofphp/php-cs-fixer: ^3.85
- gedmo/doctrine-extensions: ^3.20
- infection/infection: ^0.35
- phpbench/phpbench: ^1.3
- phpstan/phpstan: ^2.3
- phpstan/phpstan-strict-rules: ^2.0.4
- phpunit/phpunit: ^12.2
- symfony/cache: ^7.2
Suggests
- doctrine/orm: ^3.6, to load trees from the database with the Gedmo bridge
- gedmo/doctrine-extensions: ^3.20, for the Gedmo tree bridge (Bridge\Gedmo\GedmoTree)
Provides
None
Conflicts
- doctrine/orm: <3.6
- gedmo/doctrine-extensions: <3.20
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 17:05:06 UTC
README
Streaming filtering and transformation of PHP trees — nested set, adjacency list, materialized path and nested arrays — with pluggable filter strategies and Gedmo Doctrine support.
Stable since 1.0: minor and patch releases don't break the public API. Upgrading from 0.x: UPGRADE.md.
Install
composer require ghispi/tree-transform
PHP 8.4+. No runtime dependencies. Doctrine ORM 3.6+ and gedmo/doctrine-extensions 3.20+ are optional, for the
Gedmo bridge.
30-second example
Visible categories from a nested set table, as a nested array. A hidden category hides everything below it.
use Ghispi\TreeTransform\Tree; require 'vendor/autoload.php'; // Nested set rows, e.g. SELECT id, title, visible, lft, rgt FROM category ORDER BY lft $rows = [ ['id' => 1, 'title' => 'Shop', 'visible' => true, 'lft' => 1, 'rgt' => 12], ['id' => 2, 'title' => 'Clothing', 'visible' => true, 'lft' => 2, 'rgt' => 7], ['id' => 3, 'title' => 'Men', 'visible' => true, 'lft' => 3, 'rgt' => 4], ['id' => 4, 'title' => 'Women', 'visible' => false, 'lft' => 5, 'rgt' => 6], ['id' => 5, 'title' => 'Electronics', 'visible' => true, 'lft' => 8, 'rgt' => 11], ['id' => 6, 'title' => 'Phones', 'visible' => true, 'lft' => 9, 'rgt' => 10], ]; $menu = Tree::fromNestedSet($rows) ->filter(fn(array $category) => $category['visible']) ->map(fn(array $category) => ['title' => $category['title']]) ->toNestedArray(omitEmptyChildren: true); echo json_encode($menu, JSON_PRETTY_PRINT), "\n";
Output:
[
{
"title": "Shop",
"children": [
{
"title": "Clothing",
"children": [
{
"title": "Men"
}
]
},
{
"title": "Electronics",
"children": [
{
"title": "Phones"
}
]
}
]
}
]
Source: examples/getting-started-quickstart.php
Features
- Any input shape: nested set, adjacency list (any order or pre-ordered), materialized path, nested arrays, objects and Doctrine entities. Several roots, validation of the input, configurable field names.
- Filter strategies for what happens to subtrees and ancestors:
Prune— a non-matching node is removed with its subtree (the default),KeepAncestors— matches with their path to the root (search in a tree),Promote— remove non-matching nodes, move matching descendants up,ActiveTrail— a navigation menu expanded down to the current page, or a breadcrumb,Annotate— keep everything, mark hits, their ancestors and their descendants,MinMatches— keep nodes whose subtree has at least n matches,Placeholder— likePrune, with a stub for each run of hidden nodes,OnMatch— keep, test or drop the descendants of a match.
- Predicates: closures with access to depth and a lazy view of the ancestors,
AllOf/AnyOf/Not,FieldEquals,FieldIn,DepthRange. - Stages:
map,maxDepth,depthWindow(re-rooting),limit,sortSiblings. - Outputs: nested arrays (Gedmo
__childrencompatible), streamingiterate(), flat lists, adjacency lists with rewritten parents, nested sets with recomputedlft/rgt, counts. - Streaming: for pre-ordered inputs, memory grows with tree depth, not size. Predicates never run inside a pruned subtree. Input is never modified.
- Fast and small: on PHP 8.4 a nested set is read at about 10M nodes per second and filtered with
Pruneat 6M to 8M (10M to 13M with the JIT); a million rows from a generator stream through in about 1 MiB; nested set →KeepAncestors→ nested array is about 2× faster (2.5× with the JIT) and uses 5× to 11× less memory than building the whole tree and then filtering it. Method, hardware and every number: Benchmarks. - Doctrine / Gedmo bridge: reads the Gedmo tree configuration, streams rows with the right
ORDER BY, and pushes subtree, depth and active-trail limits into SQL. - Extensible: write your own Source, FilterStrategy, Stage, Sink, Predicate or Accessor.
Documentation
Read it as a site at https://ghispi.github.io/tree-transform/ (docs of the latest release), or here:
- Overview and concepts — and how to choose a strategy
- Getting started
- Sources · Strategies · Predicates · Stages · Outputs
- Doctrine and Gedmo
- Recipes — menus, breadcrumbs, search, select boxes, empty categories
- Extending · Performance · Benchmarks
Every example in the docs is a runnable script in examples/, checked by the test suite.
Development
Everything runs in Docker — no PHP needed on the host.
make build install # build the image, install dependencies make ci # code style, PHPStan, PHPUnit make help # all targets
License
MIT, see LICENSE.