Другой, более упрощенный способ преобразования плоской структуры в $tree
в иерархию. Для его представления нужен только один временный массив:
// add children to parents
$flat = array(); # temporary array
foreach ($tree as $name => $parent)
{
$flat[$name]['name'] = $name; # self
if (NULL === $parent)
{
# no parent, is root element, assign it to $tree
$tree = &$flat[$name];
}
else
{
# has parent, add self as child
$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
: Получение модели вложенного множества в <ul>
, но скрытие «закрытого» поддеревьев . Этот ответ также показывает, что с итераторами вы достаточно гибки.
Однако это был предварительно отсортированный список, поэтому он не подходит для вашего примера. Кроме того, я всегда хотел решить эту проблему для своего рода стандартной древовидной структуры и элементов HTML <ul>
и <li>
.
Основная концепция, которую я придумал, следующая:
TreeNode
- Абстрагирует каждый элемент в простой тип TreeNode
, который может предоставить его значение (например, Name
) и узнать, есть ли у него дочерние элементы.
TreeNodesIterator
- RecursiveIterator
, способный перебирать набор (массив) из этих TreeNodes
. Это довольно просто, так как тип TreeNode
уже знает, есть ли у него потомки и какие.
RecursiveListIterator
- RecursiveIteratorIterator
, в котором есть все события, необходимые при рекурсивной итерации по любому виду RecursiveIterator
:
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
- это просто вспомогательная функция для удобного отступа в выводе. Остальные - только выходные функции для каждого события:
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);
// add children to parents
$flat = array(); # temporary array
foreach ($tree as $name => $parent)
{
$flat[$name]['name'] = $name; # self
if (NULL === $parent)
{
# no parent, is root element, assign it to $tree
$tree = &$flat[$name];
}
else
{
# has parent, add self as child
$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']);
}
/**
* @return array of child TreeNode elements
*/
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;
/**
* @var ListDecorator
*/
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)
{
// event debug code: printf("--- %'.-20s --- (Depth: %d, Element: %d)\n", $name, $this->getDepth(), @$this->elements[$this->getDepth()]);
$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());
}
Outpupt:
<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 может затем обрабатывать события.
Связанный: