
Потребность вывести многоуровневое меню — частая задача, с которой сталкивается разработчик. Основным неудобством, как правило, является процесс формирования вложенности пунктов меню при использовании в качестве основы двумерного массива. Допустим, для хранения данных структуры сайта используется дерево каталогов 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
Тогда проще использовать метод Джо Селко. Но он медленнее на вставках.