An open API service indexing awesome lists of open source software.

https://github.com/rezozero/tree-walker

Creates a configurable tree walker using different definitions for each node based on its PHP class or interface.
https://github.com/rezozero/tree-walker

graph-database php-library tree-structure walker

Last synced: 8 months ago
JSON representation

Creates a configurable tree walker using different definitions for each node based on its PHP class or interface.

Awesome Lists containing this project

README

          

# Tree Walker

[![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)

**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.

## Table of Contents

* [Usage in Twig](#usage-in-twig)
+ [Walk forward](#walk-forward)
+ [Walk backward](#walk-backward)
* [Configure your Walker](#configure-your-walker)
* [Serialization groups](#serialization-groups)
* [Stoppable definition](#stoppable-definition)

## 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`:
```twig
{# nav-item.html.twig #}


```

### Walk backward
You can *reverse* walk (aka *moon walking*) to display a page breadcrumbs for example:

```twig
{# 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 %}


{% endif %}
{% endif %}
{% endmacro %}


```

## Configure your Walker

1. 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.
2. Create a custom *Walker* class **extending** `AbstractWalker`.
You’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`.**
3. 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()->count()`.
4. Instantiate your custom Walker with your root item, and your context object

Here is some pseudo PHP code example:

```php
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 *symfony/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 recursion
- `children`: 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.

```php
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.