{"id":32613856,"url":"https://github.com/rezozero/tree-walker","last_synced_at":"2025-10-30T15:55:09.761Z","repository":{"id":62535352,"uuid":"268506672","full_name":"rezozero/tree-walker","owner":"rezozero","description":"Creates a configurable tree walker using different definitions for each node based on its PHP class or interface.","archived":false,"fork":false,"pushed_at":"2025-02-10T23:23:50.000Z","size":128,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-02-11T00:25:54.319Z","etag":null,"topics":["graph-database","php-library","tree-structure","walker"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rezozero.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-01T11:46:54.000Z","updated_at":"2025-02-10T23:23:54.000Z","dependencies_parsed_at":"2025-02-11T00:23:33.471Z","dependency_job_id":"4b981193-a027-4235-9dcc-fcc4eb3e9aa5","html_url":"https://github.com/rezozero/tree-walker","commit_stats":null,"previous_names":[],"tags_count":31,"template":false,"template_full_name":null,"purl":"pkg:github/rezozero/tree-walker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Ftree-walker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Ftree-walker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Ftree-walker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Ftree-walker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rezozero","download_url":"https://codeload.github.com/rezozero/tree-walker/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezozero%2Ftree-walker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281835582,"owners_count":26569857,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-30T02:00:06.501Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["graph-database","php-library","tree-structure","walker"],"created_at":"2025-10-30T15:55:00.689Z","updated_at":"2025-10-30T15:55:09.755Z","avatar_url":"https://github.com/rezozero.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tree Walker\n\n[![Tests status](https://github.com/rezozero/tree-walker/actions/workflows/run-test.yml/badge.svg)](https://github.com/rezozero/tree-walker/actions/workflows/run-test.yml) ![License](http://img.shields.io/:license-mit-blue.svg?style=flat) [![Packagist](https://img.shields.io/packagist/v/rezozero/tree-walker.svg?style=flat)](https://packagist.org/packages/rezozero/tree-walker)\n\n**Creates a configurable tree walker using different definitions for each node based on its PHP class or interface.**\n\n`WalkerInterface` implements `\\Countable` in order to use it seamlessly in your PHP code and Twig templates. Each `WalkerInterface` will carry your *node* object and its children.\n\nSince v1.1.0 `AbstractWalker` does not implement `\\IteratorAggregate` in order to be compatible with *api-platform* normalizer (it normalizes it as a Hydra:Collection).\nBut if you need it in you can add `\\IteratorAggregate` to your custom Walker implementation, `getIterator` is already implemented.\n\nIf your application may introduce cyclic references between objects, you can use `AbstractCycleAwareWalker` instead of `AbstractWalker` to keep track of collected items and prevent\ncollecting same item children twice. Collision detection is based on `spl_object_id` method.\n\n## Table of Contents\n\n* [Usage in Twig](#usage-in-twig)\n    + [Walk forward](#walk-forward)\n    + [Walk backward](#walk-backward)\n* [Configure your Walker](#configure-your-walker)\n* [Serialization groups](#serialization-groups)\n* [Stoppable definition](#stoppable-definition)\n\n## Usage in Twig\n\n- First, make sure your Walker instance implements `\\IteratorAggregate` in order to use it directly into a loop\n\n### Walk forward\nHere is an example of a **recursive** navigation item template using our `WalkerInterface`:\n```twig\n{# nav-item.html.twig #}\n\u003cli class=\"nav-item\"\u003e\n    \u003cspan\u003e{{ item.title }}\u003c/span\u003e\n    {# \n     # Walker object must be your general navigation WalkerInterface \n     # and current page must be inside navigation graph.\n     #\n     # getWalkerAtItem method looks for current page in your Walker\n     # and returns walker interface for current page.\n     #}\n    {# Always a good idea to check walker item count before going further #}\n    {% if walker and walker|length %}\n        \u003cdiv class=\"dropdown-menu nav-children\"\u003e\n            \u003cul role=\"menu\"\u003e\n                {% for subWalker in walker %}\n                    {% include 'nav-item.html.twig' with {\n                        'walker': subWalker,\n                        'item' : subWalker.item,\n                    } only %}\n                {% endfor %}\n            \u003c/ul\u003e\n        \u003c/div\u003e\n    {% endif %}\n\u003c/li\u003e\n```\n\n### Walk backward\nYou can *reverse* walk (aka *moon walking*) to display a page breadcrumbs for example:\n\n```twig\n{# page.html.twig #}\n\n{% macro walkBreadcrumbs(pageWalker) %}\n    {% if pageWalker.parent %}\n        {% set pageWalker = pageWalker.parent %}\n        {# Recursive magic here … #}\n        {{ _self.walkBreadcrumbs(pageWalker) }}\n        {# Call macro itself before displaying to keep ancestors first #}\n        {% if pageWalker.item is not Neutral %}\n            \u003cli class=\"breadcrumbs-item\"\u003e\n                \u003ca href=\"{{ path(pageWalker.item) }}\"\u003e{{ pageWalker.item.title }}\u003c/a\u003e\n            \u003c/li\u003e\n        {% endif %}\n    {% endif %}\n{% endmacro %}\n\n\u003cul class=\"breadcrumbs\"\u003e\n    {# \n     # walker object must be your general navigation WalkerInterface \n     # and current page must be inside navigation graph.\n     #\n     # getWalkerAtItem method looks for current page in your Walker\n     # and returns walker interface for current page.\n     #}\n    {% set pageWalker = walker.getWalkerAtItem(page) %}\n    \n    {# Recursive magic here … #}\n    {{ _self.walkBreadcrumbs(pageWalker.getParent) }}\n    \n    \u003cli class=\"breadcrumbs-item\"\u003e{{ page.title }}\u003c/li\u003e\n\u003c/ul\u003e\n```\n\n## Configure your Walker\n\n1. Create a `WalkerContextInterface` instance to hold every service your `callable` definitions will use to fetch each tree node children. For example: a *Doctrine repository*, a *QueryBuilder*, even your *PDO* instance.\n2. Create a custom *Walker* class **extending** `AbstractWalker`.   \nYou’ll notice that `AbstractWalker` is very strict and prevents overriding its *constructor* in order to abstract all `WalkerInterface` instantiations from your business logic. **All your custom logic must be included in `definitions` and `countDefinitions`.**\n3. Add `definitions` and `countDefinitions` from your custom *Walker*. A *definition* `callable` must return an `array` (or an *iterable* object) of your items. A *countDefinition* `callable` must return an `int` representing your items number. *CountDefinitions* are optional: `AbstractWalker::count()` method will fall back on using `AbstractWalker::getChildren()-\u003ecount()`.\n4. Instantiate your custom Walker with your root item, and your context object\n\nHere is some pseudo PHP code example:\n\n```php\n\u003c?php\nuse RZ\\TreeWalker\\WalkerInterface;\nuse RZ\\TreeWalker\\WalkerContextInterface;\nuse RZ\\TreeWalker\\AbstractWalker;\nuse RZ\\TreeWalker\\Definition\\ContextualDefinitionTrait;\n\nclass Dummy\n{\n    // Current dummy identifier\n    private $id;\n    // Nested tree style current dummy parent identifier\n    private $parentDummyId;\n\n    public function hello(){\n        return 'Hey Ho!';\n    }\n\n    public function getId(){\n        return $this-\u003eid;\n    }\n}\n\nclass NotADummy\n{\n    // Nested tree style current dummy parent identifier\n    private $parentDummyId;\n\n    public function sayNothing(){\n        return '…';\n    }\n}\n\nclass DummyWalkerContext implements WalkerContextInterface\n{\n    private $dummyRepository;\n    private $notADummyRepository;\n\n    public function __construct($dummyRepository, $notADummyRepository)\n    {\n        $this-\u003edummyRepository = $dummyRepository;\n        $this-\u003enotADummyRepository = $notADummyRepository;\n    }\n\n    public function getDummyRepository()\n    {\n        return $this-\u003edummyRepository;\n    }\n\n    public function getNotADummyRepository()\n    {\n        return $this-\u003enotADummyRepository;\n    }\n}\n\nfinal class DummyChildrenDefinition\n{\n    use ContextualDefinitionTrait;\n\n    public function __invoke(Dummy $dummy, WalkerInterface $walker): array\n    {\n        if ($this-\u003econtext instanceof DummyWalkerContext) {\n            return array_merge(\n                $this-\u003econtext-\u003egetDummyRepository()-\u003efindByParentDummyId($dummy-\u003egetId()),\n                $this-\u003econtext-\u003egetNotADummyRepository()-\u003efindByParentDummyId($dummy-\u003egetId())\n            );\n        }\n        throw new \\InvalidArgumentException('Context should be instance of ' . DummyWalkerContext::class);\n    }\n}\n\nfinal class DummyWalker extends AbstractWalker implements \\IteratorAggregate\n{\n    protected function initializeDefinitions(): void\n    {\n        /*\n         * All Tree-walker logic occurs here…\n         * You are free to code any logic to fetch your item children, and\n         * to alter it given your WalkerContextInterface such as security, request…\n         */\n        $this-\u003eaddDefinition(Dummy::class, new DummyChildrenDefinition($this-\u003egetContext()));\n    }\n}\n\n/*\n * Some stupid recursive function to \n * walk entire entities tree graph\n */\nfunction everyDummySayHello(WalkerInterface $walker) {\n    if ($walker-\u003egetItem() instanceof Dummy) {\n        echo $walker-\u003egetItem()-\u003ehello();\n    }\n    if ($walker-\u003egetItem() instanceof NotADummy) {\n        echo $walker-\u003egetItem()-\u003esayNothing();\n    }\n    if ($walker-\u003ecount() \u003e 0) {\n        foreach ($walker as $childWalker) {\n            // I love recursive functions…\n            everyDummySayHello($childWalker);\n        }\n    }\n}\n\n// -------------------------------------------------------\n// Just provide some $entityManager to fetch your entities \n// from a database, a file, or your fridge…\n// -------------------------------------------------------\n$dummyRepository = $entityManager-\u003egetRepository(Dummy::class);\n$notADummyRepository = $entityManager-\u003egetRepository(NotADummy::class);\n$firstItem = $dummyRepository-\u003efindOneById(1);\n\n// Calling an AbstractWalker constructor is forbidden, always\n// use static build method\n$walker = DummyWalker::build(\n    $firstItem,\n    new DummyWalkerContext($dummyRepository, $notADummyRepository),\n    3 // max level count\n);\n\neveryDummySayHello($walker);\n```\n\n## Serialization groups\n\nAny walker interface can be serialized with *symfony/serializer* since they extends `AbstractWalker` class.\nYou should add serialization groups to ensure you do not fall into an infinite loop:\n\n- `walker`: serializes flat members with no recursion\n- `children`: triggers walker children serialization until max level is reached.\n- `children_count`: serializes children count if your application can count children array.\n- `walker_parent`: triggers reverse walker parents serialization until root is reached.\n- `walker_level`: serializes maximum and current level information.\n- `walker_metadata`: serializes current level user metadata.\n\nObviously, **do not use** `children` and `walker_parent` groups at the same time…\n\n## Stoppable definition\n\nYou may want to prevent Walker to continue after a given item definition. For example to prevent infinite loops.\nYou can write your *definition* class implementing `StoppableDefinition` interface.\n\n```php\nfinal class DummyChildrenDefinition\n{\n    use ContextualDefinitionTrait;\n    \n    public function isStoppingCollectionOnceInvoked(): bool\n    {\n        return true;\n    }\n\n    public function __invoke(Dummy $dummy, WalkerInterface $walker): array\n    {\n        // ...\n    }\n}\n```\n\nIf `isStoppingCollectionOnceInvoked` method return `true`, then each child won't have any children. It is useful when\nyou want to prevent your tree to go deeper for specific item types. This is more specific than configuring the global\n`maxLevel` value on your tree-walker root instance.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezozero%2Ftree-walker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frezozero%2Ftree-walker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezozero%2Ftree-walker/lists"}