将 $tree
中的扁平结构转换为层次结构的另一种更简化的方法。只需要一个临时数组就可以实现:
// add children to parents
$flat = array();
foreach ($tree as $name => $parent)
{
$flat[$name]['name'] = $name;
if (NULL === $parent)
{
$tree = &$flat[$name];
}
else
{
$flat[$parent]['children'][] = &$flat[$name];
}
}
unset($flat);
以上就是将层次结构转换为多维数组的全部内容:
Array
(
[children] => Array
(
[0] => Array
(
[children] => Array
(
[0] => Array
(
[name] => H
)
[1] => Array
(
[name] => F
)
)
[name] => G
)
[1] => Array
(
[name] => E
[children] => Array
(
[0] => Array
(
[name] => A
)
[1] => Array
(
[children] => Array
(
[0] => Array
(
[name] => B
)
)
[name] => C
)
)
)
)
[name] => D
)
如果你想避免递归(对于大型结构可能是负担),输出就不那么琐碎了。
我一直想解决 UL/LI "困境",以输出数组。问题在于,每个项目都不知道是否会有子项跟进或需要关闭多少之前的元素。在另一个答案中,我已经通过使用 RecursiveIteratorIterator 和查找 getDepth() 等元信息来解决了这个问题,并提供了自己编写的 Iterator:Getting nested set model into a <ul> but hiding “closed” subtrees。该答案还显示使用迭代器非常灵活。
但是,那是一个预排序列表,因此不适合您的示例。此外,我一直想为标准树结构和 HTML 的 <ul> 和 <li> 元素解决这个问题。
我提出的基本概念如下:
TreeNode
- 将每个元素抽象成一个简单的 TreeNode 类型,可以提供其值(例如 Name)以及它是否具有子项。TreeNodesIterator
- 一个能够迭代一组(数组)这些 TreeNodes 的 RecursiveIterator。这很简单,因为 TreeNode 类型已经知道它是否有子项以及哪些子项。RecursiveListIterator
- 一个具有递归迭代任何类型的 RecursiveIterator 所需的所有事件的 RecursiveIteratorIterator:- beginIteration / endIteration - 主列表的开始和结束。
- beginElement / endElement - 每个元素的开始和结束。
- beginChildren / endChildren - 每个子列表的开始和结束。
此 RecursiveListIterator 仅以函数调用的形式提供这些事件。作为 <ul><li> 列表的典型特征,子列表在其父 <li> 元素内打开和关闭。因此,endElement 事件在相应的 endChildren 事件之后触发。这可以更改或配置为扩大类的使用范围。然后,将事件分配为函数调用到装饰器对象中,以保持事物分开。ListDecorator
- 一个“装饰器”类,只是 RecursiveListIterator 事件的接收器。
我从主要的输出逻辑开始。考虑到现在是分层的 $tree 数组,最终代码如下:
$root = new TreeNode($tree);
$it = new TreeNodesIterator(array($root));
$rit = new RecursiveListIterator($it);
$decor = new ListDecorator($rit);
$rit->addDecorator($decor);
foreach($rit as $item)
{
$inset = $decor->inset(1);
printf("%s%s\n", $inset, $item->getName());
}
首先,让我们来看一下ListDecorator
,它简单地包装了<ul>
和<li>
元素,并决定列表结构的输出:
class ListDecorator
{
private $iterator;
public function __construct(RecursiveListIterator $iterator)
{
$this->iterator = $iterator;
}
public function inset($add = 0)
{
return str_repeat(' ', $this->iterator->getDepth()*2+$add);
}
构造函数接受其正在处理的列表迭代器。 "inset"只是一个用于输出缩进的辅助函数。 其余部分只是每个事件的输出函数:
构造函数接受其正在处理的列表迭代器。 inset
只是一个用于输出缩进的辅助函数。 其余部分只是每个事件的输出函数:
public function beginElement()
{
printf("%s<li>\n", $this->inset());
}
public function endElement()
{
printf("%s</li>\n", $this->inset());
}
public function beginChildren()
{
printf("%s<ul>\n", $this->inset(-1));
}
public function endChildren()
{
printf("%s</ul>\n", $this->inset(-1));
}
public function beginIteration()
{
printf("%s<ul>\n", $this->inset());
}
public function endIteration()
{
printf("%s</ul>\n", $this->inset());
}
}
考虑到这些输出函数,这是主要的输出包装/循环,我会逐步地介绍它:
$root = new TreeNode($tree);
创建根
TreeNode
,它将用于开始迭代:
$it = new TreeNodesIterator(array($root))
这个 TreeNodesIterator
是一个 RecursiveIterator
,它可以使得单个 $root
节点进行递归迭代。它作为数组传递是因为该类需要一些东西进行迭代,并允许重用具有一组子节点的数组,这些子节点也是 TreeNode
元素的数组。
$rit = new RecursiveListIterator($it);
这个 RecursiveListIterator
是一个提供了上述事件的 RecursiveIteratorIterator
。要使用它,只需要提供一个 ListDecorator
(即上面提到的类),并使用 addDecorator
进行赋值:
$decor = new ListDecorator($rit);
$rit->addDecorator($decor);
然后一切都设置好了,只需使用foreach
循环遍历它并输出每个节点:
foreach($rit as $item)
{
$inset = $decor->inset(1);
printf("%s%s\n", $inset, $item->getName());
}
正如这个示例所显示的那样,整个输出逻辑被封装在ListDecorator
类和这个单独的foreach
中。整个递归遍历已经完全封装到了SPL递归迭代器中,它们提供了一个堆栈过程,也就是说,在内部没有递归函数调用。
基于事件的ListDecorator
允许您特定地修改输出,并为相同的数据结构提供多种类型的列表。甚至可以更改输入,因为数组数据已经封装到了TreeNode
中。
完整代码示例:
<?php
namespace My;
$tree = array('H' => 'G', 'F' => 'G', 'G' => 'D', 'E' => 'D', 'A' => 'E', 'B' => 'C', 'C' => 'E', 'D' => null);
$flat = array();
foreach ($tree as $name => $parent)
{
$flat[$name]['name'] = $name;
if (NULL === $parent)
{
$tree = &$flat[$name];
}
else
{
$flat[$parent]['children'][] = &$flat[$name];
}
}
unset($flat);
class TreeNode
{
protected $data;
public function __construct(array $element)
{
if (!isset($element['name']))
throw new InvalidArgumentException('Element has no name.');
if (isset($element['children']) && !is_array($element['children']))
throw new InvalidArgumentException('Element has invalid children.');
$this->data = $element;
}
public function getName()
{
return $this->data['name'];
}
public function hasChildren()
{
return isset($this->data['children']) && count($this->data['children']);
}
public function getChildren()
{
$children = $this->hasChildren() ? $this->data['children'] : array();
$class = get_called_class();
foreach($children as &$element)
{
$element = new $class($element);
}
unset($element);
return $children;
}
}
class TreeNodesIterator implements \RecursiveIterator
{
private $nodes;
public function __construct(array $nodes)
{
$this->nodes = new \ArrayIterator($nodes);
}
public function getInnerIterator()
{
return $this->nodes;
}
public function getChildren()
{
return new TreeNodesIterator($this->nodes->current()->getChildren());
}
public function hasChildren()
{
return $this->nodes->current()->hasChildren();
}
public function rewind()
{
$this->nodes->rewind();
}
public function valid()
{
return $this->nodes->valid();
}
public function current()
{
return $this->nodes->current();
}
public function key()
{
return $this->nodes->key();
}
public function next()
{
return $this->nodes->next();
}
}
class RecursiveListIterator extends \RecursiveIteratorIterator
{
private $elements;
private $decorator;
public function addDecorator(ListDecorator $decorator)
{
$this->decorator = $decorator;
}
public function __construct($iterator, $mode = \RecursiveIteratorIterator::SELF_FIRST, $flags = 0)
{
parent::__construct($iterator, $mode, $flags);
}
private function event($name)
{
$callback = array($this->decorator, $name);
is_callable($callback) && call_user_func($callback);
}
public function beginElement()
{
$this->event('beginElement');
}
public function beginChildren()
{
$this->event('beginChildren');
}
public function endChildren()
{
$this->testEndElement();
$this->event('endChildren');
}
private function testEndElement($depthOffset = 0)
{
$depth = $this->getDepth() + $depthOffset;
isset($this->elements[$depth]) || $this->elements[$depth] = 0;
$this->elements[$depth] && $this->event('endElement');
}
public function nextElement()
{
$this->testEndElement();
$this->event('{nextElement}');
$this->event('beginElement');
$this->elements[$this->getDepth()] = 1;
}
public function beginIteration()
{
$this->event('beginIteration');
}
public function endIteration()
{
$this->testEndElement();
$this->event('endIteration');
}
}
class ListDecorator
{
private $iterator;
public function __construct(RecursiveListIterator $iterator)
{
$this->iterator = $iterator;
}
public function inset($add = 0)
{
return str_repeat(' ', $this->iterator->getDepth()*2+$add);
}
public function beginElement()
{
printf("%s<li>\n", $this->inset(1));
}
public function endElement()
{
printf("%s</li>\n", $this->inset(1));
}
public function beginChildren()
{
printf("%s<ul>\n", $this->inset());
}
public function endChildren()
{
printf("%s</ul>\n", $this->inset());
}
public function beginIteration()
{
printf("%s<ul>\n", $this->inset());
}
public function endIteration()
{
printf("%s</ul>\n", $this->inset());
}
}
$root = new TreeNode($tree);
$it = new TreeNodesIterator(array($root));
$rit = new RecursiveListIterator($it);
$decor = new ListDecorator($rit);
$rit->addDecorator($decor);
foreach($rit as $item)
{
$inset = $decor->inset(2);
printf("%s%s\n", $inset, $item->getName());
}
输出:
<ul>
<li>
D
<ul>
<li>
G
<ul>
<li>
H
</li>
<li>
F
</li>
</ul>
</li>
<li>
E
<ul>
</li>
<li>
A
</li>
<li>
C
<ul>
<li>
B
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
演示 (PHP 5.2 变体)
一个可能的变体是迭代器,它遍历任何 RecursiveIterator
并提供可以发生的所有事件的迭代。 然后 foreach 循环内的 switch / case 将处理这些事件。
相关内容: