rezozero/tree-walker

创建一个可配置的树遍历器,根据每个节点对应的PHP类或接口使用不同的方法

1.5.0 2024-07-02 20:23 UTC

README

Tests status License Packagist

创建一个可配置的树遍历器,根据每个节点对应的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>

配置你的遍历器

  1. 创建一个 WalkerContextInterface 实例来保存你的 callable 定义将使用的每个树节点子项的每个服务。例如:一个 Doctrine存储库、一个 QueryBuilder,甚至你的 PDO 实例。
  2. 创建一个自定义的 遍历器扩展 AbstractWalker
    你会注意到,AbstractWalker 非常严格,并阻止覆盖其 构造函数,以便从你的业务逻辑中抽象出所有 WalkerInterface 实例化。 所有你的自定义逻辑都必须包含在 definitionscountDefinitions 中。
  3. 从你的自定义 遍历器 添加 definitionscountDefinitions。一个 定义 callable 必须返回一个 array(或一个 可迭代 对象)的项。一个 countDefinition callable 必须返回一个表示项数量的 intCountDefinitions 是可选的:AbstractWalker::count() 方法将回退到使用 AbstractWalker::getChildren()->count()
  4. 使用你的根项和上下文对象实例化你的自定义遍历器

以下是一些伪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:序列化当前级别的用户元数据。

显然,不要同时使用 childrenwalker_parent 组...

可停止的定义

你可能想阻止遍历器在给定的项目定义后继续。例如,防止无限循环。你可以编写实现 StoppableDefinition 接口的 定义 类。

final class DummyChildrenDefinition
{
    use ContextualDefinitionTrait;
    
    public function isStoppingCollectionOnceInvoked(): bool
    {
        return true;
    }

    public function __invoke(Dummy $dummy, WalkerInterface $walker): array
    {
        // ...
    }
}

如果 isStoppingCollectionOnceInvoked 方法返回 true,则每个子节点将没有任何子节点。当你想阻止你的树对特定项目类型进行更深的遍历时,这很有用。这比在树遍历的根实例上配置全局的 maxLevel 值更具体。