rezozero / tree-walker
Creates a configurable tree walker using different methods for each node based on its PHP class or interface
Installs: 6 934
Dependents: 3
Suggesters: 0
Security: 0
Stars: 1
Watchers: 4
Forks: 0
Open Issues: 0
Requires
- php: >=8.1
- doctrine/collections: >=1.6
- jms/serializer: ^3.7
- psr/cache: ^1.0 || ^2.0 || ^3.0
- symfony/cache: >=5.4
- symfony/serializer: >=5.4
Requires (Dev)
- phpstan/phpstan: ^1.8.2
- phpunit/phpunit: ^9.5
- squizlabs/php_codesniffer: ^3.3
- symfony/property-access: >=5.4
README
Creates a configurable tree walker using different definitions for each node based on its PHP class or interface.
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.
Since v1.1.0 AbstractWalker
does not implement \IteratorAggregate
in order to be compatible with api-platform normalizer (it normalizes it as a Hydra:Collection).
But if you need it in you can add \IteratorAggregate
to your custom Walker implementation, getIterator
is already implemented.
If your application may introduce cyclic references between objects, you can use AbstractCycleAwareWalker
instead of AbstractWalker
to keep track of collected items and prevent
collecting same item children twice. Collision detection is based on spl_object_id
method.
Usage in Twig
- First, make sure your Walker instance implements
\IteratorAggregate
in order to use it directly into a loop
Walk forward
Here is an example of a recursive navigation item template using our WalkerInterface
:
{# nav-item.html.twig #} <li class="nav-item"> <span>{{ item.title }}</span> {# # Walker object must be your general navigation WalkerInterface # and current page must be inside navigation graph. # # getWalkerAtItem method looks for current page in your Walker # and returns walker interface for current page. #} {# Always a good idea to check walker item count before going further #} {% if walker and walker|length %} <div class="dropdown-menu nav-children"> <ul role="menu"> {% for subWalker in walker %} {% include 'nav-item.html.twig' with { 'walker': subWalker, 'item' : subWalker.item, } only %} {% endfor %} </ul> </div> {% endif %} </li>
Walk backward
You can reverse walk (aka moon walking) to display a page breadcrumbs for example:
{# page.html.twig #} {% macro walkBreadcrumbs(pageWalker) %} {% if pageWalker.parent %} {% set pageWalker = pageWalker.parent %} {# Recursive magic here … #} {{ _self.walkBreadcrumbs(pageWalker) }} {# Call macro itself before displaying to keep ancestors first #} {% if pageWalker.item is not Neutral %} <li class="breadcrumbs-item"> <a href="{{ path(pageWalker.item) }}">{{ pageWalker.item.title }}</a> </li> {% endif %} {% endif %} {% endmacro %} <ul class="breadcrumbs"> {# # walker object must be your general navigation WalkerInterface # and current page must be inside navigation graph. # # getWalkerAtItem method looks for current page in your Walker # and returns walker interface for current page. #} {% set pageWalker = walker.getWalkerAtItem(page) %} {# Recursive magic here … #} {{ _self.walkBreadcrumbs(pageWalker.getParent) }} <li class="breadcrumbs-item">{{ page.title }}</li> </ul>
Configure your Walker
- Create a
WalkerContextInterface
instance to hold every service yourcallable
definitions will use to fetch each tree node children. For example: a Doctrine repository, a QueryBuilder, even your PDO instance. - Create a custom Walker class extending
AbstractWalker
.
You’ll notice thatAbstractWalker
is very strict and prevents overriding its constructor in order to abstract allWalkerInterface
instantiations from your business logic. All your custom logic must be included indefinitions
andcountDefinitions
. - Add
definitions
andcountDefinitions
from your custom Walker. A definitioncallable
must return anarray
(or an iterable object) of your items. A countDefinitioncallable
must return anint
representing your items number. CountDefinitions are optional:AbstractWalker::count()
method will fall back on usingAbstractWalker::getChildren()->count()
. - Instantiate your custom Walker with your root item, and your context object
Here is some pseudo PHP code example:
<?php use RZ\TreeWalker\WalkerInterface; use RZ\TreeWalker\WalkerContextInterface; use RZ\TreeWalker\AbstractWalker; use RZ\TreeWalker\Definition\ContextualDefinitionTrait; class Dummy { // Current dummy identifier private $id; // Nested tree style current dummy parent identifier private $parentDummyId; public function hello(){ return 'Hey Ho!'; } public function getId(){ return $this->id; } } class NotADummy { // Nested tree style current dummy parent identifier private $parentDummyId; public function sayNothing(){ return '…'; } } class DummyWalkerContext implements WalkerContextInterface { private $dummyRepository; private $notADummyRepository; public function __construct($dummyRepository, $notADummyRepository) { $this->dummyRepository = $dummyRepository; $this->notADummyRepository = $notADummyRepository; } public function getDummyRepository() { return $this->dummyRepository; } public function getNotADummyRepository() { return $this->notADummyRepository; } } final class DummyChildrenDefinition { use ContextualDefinitionTrait; public function __invoke(Dummy $dummy, WalkerInterface $walker): array { if ($this->context instanceof DummyWalkerContext) { return array_merge( $this->context->getDummyRepository()->findByParentDummyId($dummy->getId()), $this->context->getNotADummyRepository()->findByParentDummyId($dummy->getId()) ); } throw new \InvalidArgumentException('Context should be instance of ' . DummyWalkerContext::class); } } final class DummyWalker extends AbstractWalker implements \IteratorAggregate { protected function initializeDefinitions(): void { /* * All Tree-walker logic occurs here… * You are free to code any logic to fetch your item children, and * to alter it given your WalkerContextInterface such as security, request… */ $this->addDefinition(Dummy::class, new DummyChildrenDefinition($this->getContext())); } } /* * Some stupid recursive function to * walk entire entities tree graph */ function everyDummySayHello(WalkerInterface $walker) { if ($walker->getItem() instanceof Dummy) { echo $walker->getItem()->hello(); } if ($walker->getItem() instanceof NotADummy) { echo $walker->getItem()->sayNothing(); } if ($walker->count() > 0) { foreach ($walker as $childWalker) { // I love recursive functions… everyDummySayHello($childWalker); } } } // ------------------------------------------------------- // Just provide some $entityManager to fetch your entities // from a database, a file, or your fridge… // ------------------------------------------------------- $dummyRepository = $entityManager->getRepository(Dummy::class); $notADummyRepository = $entityManager->getRepository(NotADummy::class); $firstItem = $dummyRepository->findOneById(1); // Calling an AbstractWalker constructor is forbidden, always // use static build method $walker = DummyWalker::build( $firstItem, new DummyWalkerContext($dummyRepository, $notADummyRepository), 3 // max level count ); everyDummySayHello($walker);
Serialization groups
Any walker interface can be serialized with jms/serializer since they extends AbstractWalker
class.
You should add serialization groups to ensure you do not fall into an infinite loop:
walker
: serializes flat members with no recursionchildren
: triggers walker children serialization until max level is reached.children_count
: serializes children count if your application can count children array.walker_parent
: triggers reverse walker parents serialization until root is reached.walker_level
: serializes maximum and current level information.walker_metadata
: serializes current level user metadata.
Obviously, do not use children
and walker_parent
groups at the same time…
Stoppable definition
You may want to prevent Walker to continue after a given item definition. For example to prevent infinite loops.
You can write your definition class implementing StoppableDefinition
interface.
final class DummyChildrenDefinition { use ContextualDefinitionTrait; public function isStoppingCollectionOnceInvoked(): bool { return true; } public function __invoke(Dummy $dummy, WalkerInterface $walker): array { // ... } }
If isStoppingCollectionOnceInvoked
method return true
, then each child won't have any children. It is useful when
you want to prevent your tree to go deeper for specific item types. This is more specific than configuring the global
maxLevel
value on your tree-walker root instance.