rezozero / tree-walker
创建一个可配置的树遍历器,根据每个节点对应的PHP类或接口使用不同的方法
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
创建一个可配置的树遍历器,根据每个节点对应的PHP类或接口使用不同的定义。
WalkerInterface
实现 \Countable
以便无缝地在您的PHP代码和Twig模板中使用。每个 WalkerInterface
都将携带您的 节点 对象及其子节点。
自v1.1.0以来,AbstractWalker
不实现 \IteratorAggregate
以与 api-platform 规范化器兼容(它将其规范化为Hydra:Collection)。但如果你需要,你可以将 \IteratorAggregate
添加到你的自定义遍历器实现中,getIterator
已实现。
如果你的应用程序可能在对象之间引入循环引用,你可以使用 AbstractCycleAwareWalker
而不是 AbstractWalker
来跟踪收集的项目并防止收集相同项目的子项目两次。冲突检测基于 spl_object_id
方法。
在Twig中使用
- 首先,确保你的遍历器实例实现了
\IteratorAggregate
以便可以直接在循环中使用它
正向遍历
以下是一个使用我们的 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>
反向遍历
你可以 反转 遍历(又称 月亮行走)以显示页面面包屑等
{# 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>
配置你的遍历器
- 创建一个
WalkerContextInterface
实例来保存你的callable
定义将使用的每个树节点子项的每个服务。例如:一个 Doctrine存储库、一个 QueryBuilder,甚至你的 PDO 实例。 - 创建一个自定义的 遍历器 类 扩展
AbstractWalker
。
你会注意到,AbstractWalker
非常严格,并阻止覆盖其 构造函数,以便从你的业务逻辑中抽象出所有WalkerInterface
实例化。 所有你的自定义逻辑都必须包含在definitions
和countDefinitions
中。 - 从你的自定义 遍历器 添加
definitions
和countDefinitions
。一个 定义callable
必须返回一个array
(或一个 可迭代 对象)的项。一个 countDefinitioncallable
必须返回一个表示项数量的int
。 CountDefinitions 是可选的:AbstractWalker::count()
方法将回退到使用AbstractWalker::getChildren()->count()
。 - 使用你的根项和上下文对象实例化你的自定义遍历器
以下是一些伪PHP代码示例
<?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);
序列化组
任何遍历器接口都可以使用 jms/serializer 序列化,因为它们扩展了 AbstractWalker
类。你应该添加序列化组以确保不会陷入无限循环
walker
:不递归序列化平坦成员children
:触发遍历器子成员序列化,直到达到最大级别。children_count
:如果您的应用程序可以计数子数组,则序列化子成员数量。walker_parent
:触发反向遍历器父成员序列化,直到达到根。walker_level
:序列化最大和当前级别信息。walker_metadata
:序列化当前级别的用户元数据。
显然,不要同时使用 children
和 walker_parent
组...
可停止的定义
你可能想阻止遍历器在给定的项目定义后继续。例如,防止无限循环。你可以编写实现 StoppableDefinition
接口的 定义 类。
final class DummyChildrenDefinition { use ContextualDefinitionTrait; public function isStoppingCollectionOnceInvoked(): bool { return true; } public function __invoke(Dummy $dummy, WalkerInterface $walker): array { // ... } }
如果 isStoppingCollectionOnceInvoked
方法返回 true
,则每个子节点将没有任何子节点。当你想阻止你的树对特定项目类型进行更深的遍历时,这很有用。这比在树遍历的根实例上配置全局的 maxLevel
值更具体。