
Потребность вывести многоуровневое меню — частая задача, с которой сталкивается разработчик. Основным неудобством, как правило, является процесс формирования вложенности пунктов меню при использовании в качестве основы двумерного массива. Допустим, для хранения данных структуры сайта используется дерево каталогов Nestes Sets. А в целях реализации ЧПУ, для каждого раздела сайта хранится свой абсолютный путь:
// примерный массив с элементами меню, получаемый в результате выборки из БД
Array
(
[0] => Array
(
[lkey] => 10
[title] => О компании
[path] => /about
)
[1] => Array
(
[lkey] => 11
[title] => История
[path] => /about/history
)
[2] => Array
(
[lkey] => 12
[title] => Контактная информация
[path] => /about/contacts
)
[3] => Array
(
[lkey] => 13
[title] => Пресс-центр
[path] => /press
)
[4] => Array
(
[lkey] => 14
[title] => Новости
[path] => /press/news
)
[5] => Array
(
[lkey] => 15
[title] => Новости компании
[path] => /press/news/company
)
[6] => Array
(
[lkey] => 16
[title] => Новости отрасли
[path] => /press/news/branch
)
)
Назовём этот массив $menuArray. А теперь представим, что из него нужно сформировать следующий код:
<ul>
<li>
<a href="/about">О компании</a>
<ul>
<li>
<a href="/about/history">История</a>
</li>
<li>
<a href="/about/contacts">Контактная информация</a>
</li>
</ul>
</li>
<li>
<a href="/press">Пресс-центр</a>
<ul>
<li>
<a href="/press/news">Новости</a>
<ul>
<li>
<a href="/press/news/company">Новости компании</a>
</li>
<li>
<a href="/press/news/branch">Новости отрасли</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
Думаю, многие согласятся с утверждением, что выводить многоуровневое меню с помощью рекурсии гораздо удобнее, чем напрямую обрабатывать двумерный массив. Ведь во втором случае приходится ломать голову, когда нужно открыть вложенный список UL, когда его закрыть и сколько раз. Если Вам эти слова ни о чём не говорят, то Вы гений или — что более вероятно — пока что не сталкивались с такой задачей.
Но для того, чтобы использовать рекурсионный вызов, необходимо иметь массив с иерархической вложенностью данных о пунктах меню. Примерно так должен выглядеть массив $menuTree:
// дочерние элементы каждого пункта меню размещены в ключах childNodes
Array
(
[about] => Array
(
[lkey] => 10
[title] => О компании
[path] => /about
[childNodes] => Array
(
[history] => Array
(
[lkey] => 11
[title] => История
[path] => /about/history
)
[contacts] => Array
(
[lkey] => 12
[title] => Контактная информация
[path] => /about/contacts
)
)
)
[press] => Array
(
[lkey] => 13
[title] => Пресс-центр
[path] => /press
[childNodes] => Array
(
[news] => Array
(
[lkey] => 14
[title] => Новости
[path] => /press/news
[childNodes] => Array
(
[company] => Array
(
[lkey] => 15
[title] => Новости компании
[path] => /press/news/company
)
[branch] => Array
(
[lkey] => 16
[title] => Новости отрасли
[path] => /press/news/branch
)
)
)
)
)
)
Имея такой массив с иерархической вложенностью элементов, нетрудно написать функцию для вывода меню и воспользоваться ею.
// функция вывода меню
function print_menu($menu)
{
echo '<ul>';
foreach ($menu as $key => $item)
{
echo '<li>';
echo '<a href="'.$item['path'].'">'.$item['title'].'</a>';
if (!empty($item['childNodes']))
{
print_menu($item['childNodes']);
}
echo '</li>';
}
echo '</ul>';
}
// чтобы отобразить меню, нужно вызвать рассмотренную функцию
print_menu($menu);
Данный подход намного элегантнее формирования меню из двумерного массива, потому что не возникает путаницы: когда открывать и закрывать вложенные списки; а объём кода раза в два меньше (а то и в три). И речь идёт не о сэкономленных байтах — в коротком коде проще разобраться.
Внимание! Как из двумерного массива получить массив с иерархической вложенностью?
Легко. Необходимо воспользоваться функцией, которая была создана на основании статьи Convert anything to Tree Structures in PHP:
/**
* Построение массива с иерархической вложенностью элементов.
*
* @param array $array Исходный массив
* @param string $value_key Элемент, который содержит строку для разбора
* @return bool|array
*/
function buildLevelTree($array, $value_key)
{
if (!is_array($array)) return FALSE;
// регулярное выражение для формирования дерева
$splitRE = "///";
// исходное пустое дерево
$tree = array();
// меняем ключи у исходного массива
$array_mod_keys = array();
while (list($key, $val) = each($array))
{
$array_mod_keys[$val[$value_key]] = $val;
}
// заменяем исходный массив новым (с обновлёнными ключами)
$array = $array_mod_keys;
unset($array_mod_keys);
// формируем дерево
foreach ($array as $key => $val)
{
// определяем родительские ($parts)и текущий ($leafPart) сегменты
$parts = preg_split($splitRE, $key, -1, PREG_SPLIT_NO_EMPTY);
$leafPart = array_pop($parts);
// построение родительской структуры (для очень глубоких структур может работать медленно)
$parentArr = &$tree;
foreach ($parts as $part)
{
if (!isset($parentArr[$part]))
{
$parentArr[$part] = array();
}
elseif (!is_array($parentArr[$part]))
{
$parentArr[$part] = array();
}
if (!empty($parentArr[$part]))
{
$parentArr = &$parentArr[$part]['childNodes'];
}
}
// добавление финального сегмента в структуру
if (empty($parentArr[$leafPart]))
{
$parentArr[$leafPart] = $val;
}
}
// удаляем пустые элементы
foreach ($tree as $k => $val)
{
if (empty($val))
unset($tree[$k]);
}
// возвращаем сгенерированный результат
return $tree;
}
Пример использования
Итак, у нас есть исходный массив $menuArray, функция вывода меню print_menu() и функция buildLevelTree() для преобразования исходных данных в массив с иерархической вложенностью. Воспользоваться таким арсеналом легко и приятно:
// формируем массив с иерархической вложенностью; // второй параметр функции определяет, // в каком ключе элемента меню содержится значение пути к нему $menuTree = buildLevelTree($menuArray, 'path'); // выводим меню print_menu($menuTree);
Автор: Никита Мосияш.
Комментарии:
stanis
Nested sets, Никита, nested sets. Спеллчеккеры рулят — особенно, если быстро печатаешь.
irgik
Здравствуйте. Спасибо огромное за статью. Все отлично, но есть один вопрос:
для хранения дерева я использую метод материализованых путей. Возможно ли как-то модифицировать скрипт, чтоб была возможность выводить не только все дерево, а еще и его части (к примеру начиная со второго уровня и до конца)?
stanis
Тогда проще использовать метод Джо Селко. Но он медленнее на вставках.
