Меню изнутри - файлы, типы, сборка пунктов, кэш
Разбираем меню по слоям: где физически лежат пункты, как компонент обходит дерево разделов и что от этого обхода попадает в кэш. Дальше смотрим, каким меню приходит в шаблон и чем оно отличается от хлебных крошек.
Механика
Пункты меню лежат в файлах внутри папок разделов, а не в таблицах базы данных.
Имя файла собрано из точки, типа меню и суффикса: файл .left.menu.php описывает
меню типа left. Тип меню - это идентификатор латиницей, и с местом вывода на
странице он никак не связан.
Сам тип регистрируется в настройках модуля управления структурой, отдельно для каждого сайта. Компонент читает файл по имени и работает даже с незарегистрированным типом, а вот интерфейс административной части показывает только заведённые типы меню.
Один пункт статического меню - это массив ровно из пяти элементов с закреплённым смыслом. По порядку: название, адрес, массив дополнительных адресов для подсветки, массив произвольных параметров и условие показа. Последний элемент - строка кода на PHP, которая должна вернуть истину.
Файл расширения .тип.menu_ext.php отличается от обычного моментом и способом
работы. Он не хранит готовые пункты, а исполняется компонентом во время сборки
меню и дописывает свои пункты в тот же массив. Компонент читает такой файл
только при включённом параметре подключения расширений.
Поиск файла меню идёт снизу вверх: текущий раздел, родительский, дальше до корня сайта. Обход останавливается на первом найденном файле независимо от его содержимого, поэтому меню раздела без своего файла - это меню родителя. Статический файл заменяет только статический, а файл расширения - только расширение.
Активность пункта компонент определяет сравнением адресов, а не структурой
разделов. Совпал адрес пункта с адресом текущей страницы - в поле SELECTED
приходит истина. Третий элемент массива добавляет адреса, при которых пункт
считается активным тоже: так подсвечивают раздел на страницах его элементов.
В кэш меню попадает и собранный список пунктов, и готовая разметка шаблона. Ключ кэша учитывает адрес страницы, тип меню, группы посетителя и значимые переменные запроса, поэтому вариантов накапливается примерно столько же, сколько страниц. На попадании в кэш шаблон повторно не выполняется вовсе.
Шаблону компонент отдаёт плоский массив $arResult, а не вложенное дерево.
Вложенность передаётся полем DEPTH_LEVEL каждого пункта, и разметку с
вложенными списками шаблон строит сам. Рядом с полями уровня приходят TEXT,
LINK, SELECTED, ITEM_TYPE, PARAMS и PERMISSION.
Своя разметка живёт в копии шаблона внутри шаблона сайта, а правка данных - в
файле result_modifier.php рядом с ней. Первый отвечает за представление, второй
меняет $arResult до вывода и попадает в тот же кэш. Штатный шаблон компонента
при этом остаётся нетронутым и обновляемым.
Хлебные крошки собираются из другого источника и файлов меню не читают вовсе. Цепочку наполняет платформа заголовками разделов пути и вызовами из кода страницы, а компонент только рисует накопленное. Поэтому меню и цепочка на одной странице расходятся легко и совершенно законно.
Шаги
- Посмотреть тип меню в параметрах компонента и найти файлы этого типа в дереве разделов.
- Пройти от текущего раздела вверх до корня и определить, какой файл выигрывает поиск.
- Открыть выигравший файл и разобрать его пункты по пяти элементам массива.
- Проверить, подключается ли файл расширения и что именно он дописывает в массив пунктов.
- Посмотреть, каким массив пришёл в шаблон, и только после этого менять разметку.
Код
Ищем файлы меню в дереве разделов:
find /home/bitrix/www -maxdepth 3 -name '.*.menu.php' -o -maxdepth 3 -name '.*.menu_ext.php'# /.top.menu.php - статическое меню типа top в корне сайта# /catalog/.left.menu.php - меню типа left, действует на /catalog/ и нижеФайл в разделе перекрывает родительский полностью, поэтому список найденных путей отвечает на вопрос о наследовании быстрее любого чтения кода. Пустой файл в списке выглядит так же, как заполненный.
Разбираем пункт статического меню:
$aMenuLinks = [ ['Каталог', '/catalog/', // название и адрес пункта ['/catalog/index.php', '/catalog/all/'], // адреса, при которых пункт активен ['CLASS' => 'is-wide'], // произвольные параметры, приходят в $PARAMS 'CSite::InGroup([1, 6])'], // условие показа: выражение с истиной];Число доступных дополнительных параметров задаётся настройкой модуля управления структурой. Условие в пятом элементе вычисляется на каждой сборке меню, поэтому тяжёлые проверки в нём обходятся дороже, чем кажется.
Дописываем пункты файлом расширения:
// /catalog/.left.menu_ext.php - исполняется во время сборки меню, а не при правкеif (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();global $APPLICATION;$aMenuLinksExt = $APPLICATION->IncludeComponent('bitrix:menu.sections', '', [ 'IBLOCK_TYPE' => 'catalog', 'IBLOCK_ID' => 5, 'SECTION_URL' => '/catalog/#SECTION_CODE#/', 'CACHE_TIME' => 3600,]);$aMenuLinks = array_merge($aMenuLinks, $aMenuLinksExt); // дополняем, а не заменяемПрисваивание вместо слияния молча выбрасывает статические пункты раздела. Пустой файл расширения тоже осмыслен: он подавляет унаследованное динамическое меню и оставляет от него только статику.
Задаём компоненту типы, глубину и кэш:
$APPLICATION->IncludeComponent('bitrix:menu', 'horizontal_multilevel', [ 'ROOT_MENU_TYPE' => 'top', // тип меню первого уровня 'CHILD_MENU_TYPE' => 'left', // тип меню вложенных уровней 'MAX_LEVEL' => 3, 'USE_EXT' => 'Y', // без этого расширения не читаются 'MENU_CACHE_TYPE' => 'A', 'MENU_CACHE_USE_GROUPS' => 'Y', // свой вариант кэша на каждый набор групп 'MENU_CACHE_GET_VARS' => ['SECTION_ID'], // значимые переменные запроса]);Типы первого и вложенных уровней различаются: одинаковый тип на обоих уровнях даёт повторение корневых пунктов внутри разделов. Значимые переменные нужны там, где адреса разделов идут без ЧПУ и различаются только параметрами.
Смотрим, что компонент отдал шаблону:
// $arResult - плоский список пунктов, вложенность лежит в DEPTH_LEVELforeach ($arResult as $item) { printf("%s%s %s selected=%s type=%s\n", str_repeat('. ', $item['DEPTH_LEVEL']), $item['TEXT'], $item['LINK'], $item['SELECTED'] ? 'Y' : 'N', $item['ITEM_TYPE']);}// ITEM_TYPE: D раздел, P страница, U страница с параметрамиТакой вывод сразу показывает, дошли ли динамические пункты и какой из них
активен. Пустой $arResult при заполненном файле означает несовпадение типа
меню, а не ошибку сборки.
Ставим свою разметку в копии шаблона:
<?if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();?><ul class="menu"><?foreach ($arResult as $arItem):?> <li class="lvl-<?=$arItem['DEPTH_LEVEL']?><?=$arItem['SELECTED'] ? ' is-active' : ''?>"> <a href="<?=$arItem['LINK']?>"><?=htmlspecialcharsbx($arItem['TEXT'])?></a> </li><?endforeach?></ul>Копию шаблона кладут в шаблон сайта: правка штатного шаблона теряется на первом же обновлении платформы. Названия из инфоблока экранируют, потому что их задаёт контент-менеджер, а не разработчик.
Правим само дерево до вывода:
// result_modifier.php рядом с template.phpforeach ($arResult as $key => $item) { if (($item['PARAMS']['HIDE'] ?? '') === 'Y') { unset($arResult[$key]); // свой признак, заданный четвёртым элементом пункта }}// правки отсюда попадают в кэш вместе с готовой разметкой шаблонаЛогика в этом файле отделена от разметки и переживает обновление штатного шаблона. Всё, что нельзя кэшировать, выносят в эпилог шаблона: он выполняется на каждом хите независимо от кэша.
Сравниваем источник цепочки навигации:
// цепочку наполняют заголовки разделов пути и вызовы из кода страницы$APPLICATION->AddChainItem('Акции сентября', '/catalog/sale/');$APPLICATION->IncludeComponent('bitrix:breadcrumb', '', ['START_FROM' => '0']);// файлы меню в цепочку не попадают, а её пункты не влияют на менюМеню и цепочка расходятся на одной странице совершенно штатно. Первое собрано из файлов разделов, вторая - из заголовков и вызовов, и общего источника у них нет.
Ограничения
Меню не проверяет, существует ли адрес пункта. При удалении раздела или страницы пункт остаётся на месте и ведёт на страницу с ошибкой, пока его не уберут руками.
Штатные шаблоны компонента рисуют максимум четыре уровня вложенности. Более глубокое дерево требует своей копии шаблона с доработанной разметкой и стилями выпадающих списков.
Поле PERMISSION заполняется только у меню сайта и содержит уровень доступа к
разделу. В других компонентах, которые тоже отдают пункты, этого ключа нет
вовсе, и опираться на него нельзя.
Условие показа «Для папки и файла» работает только на статических страницах и адресах с ЧПУ. Для динамических адресов берут условие по параметру в адресе, иначе пункт не подсветится никогда.
Кэш меню зависит от адреса страницы, поэтому на большом каталоге вариантов накапливаются сотни тысяч. Отключение кэша эту проблему не решает: выборки расширения тогда идут на каждом показе страницы.
Типичные проблемы
В подразделе своё меню, хотя файл там не заводили.
Страницу создавали с включённой опцией добавления пункта меню. Она молча заводит файл меню прямо в разделе, и тот перекрывает родительский.
Меню на сайте работает, а панель администратора его не видит.
Тип меню не заведён в настройках модуля управления структурой этого сайта. Компонент читает файл по имени, а интерфейс показывает только зарегистрированные типы.
После включения кэша пропали стили шаблона меню.
Стили подключены кодом внутри шаблона, а на попадании в кэш шаблон не выполняется. Файл стилей рядом с разметкой шаблона платформа подключает сама.
Под администратором меню верное, у гостя пункты другие.
Кэш меню собирается отдельно для каждого набора групп посетителя. Проверять правку нужно в режиме неавторизованного посетителя, а не из-под своей учётной записи.
Правки шаблона меню пропали после обновления платформы.
Шаблон правили прямо в каталоге ядра, и обновление вернуло ему исходный вид. Шаблон копируют в шаблон сайта, там обновления его больше не трогают.
Поле активности у пунктов всегда пустое.
Адрес пункта не совпадает с адресом текущей страницы буква в букву. Дополнительные адреса для подсветки перечисляют третьим элементом массива пункта.
Частые вопросы
Где лежит шаблон компонента меню?
Штатные шаблоны - в папке самого компонента, свои - в шаблоне сайта. Поиск начинается с каталога компонентов текущего шаблона и берёт первый найденный.
Чем .menu.php отличается от .menu_ext.php?
Первый хранит готовый массив пунктов, второй собирает пункты кодом во время сборки меню. Второй читается только при включённом параметре подключения расширений.
Как заставить компонент брать меню из корня, а не из раздела?
Никак: обход идёт вверх и останавливается на первом найденном файле. Лишние файлы меню в разделах удаляют, и тогда компонент доходит до корня.
Откуда меню берёт вложенные уровни?
Из файлов меню вложенных разделов того типа, который задан для дочерних уровней. Сам файл раздела уровней не содержит: это плоский список пунктов.
Можно ли добавить пункт в административное меню файлом меню?
Файл меню админки лежит в каталоге ядра, и правка теряется при обновлении. Свои пункты административного меню добавляют обработчиком события из своего модуля.
Смежное
- Меню и хлебные крошки - оглавление подтемы
- Меню и навигация - устройство меню целиком
- Многоуровневое меню: свои пункты и вывод из инфоблока - как вывести уровни и пункты из данных
- Кэш меню: разрастание каталога, тяжёлые запросы, настройка - что делать с числом вариантов кэша
- Меню показывает не то: разбор причин - разбор конкретного сбоя меню
- Хлебные крошки: правка цепочки и свои элементы - вторая часть навигации
- Компонент изнутри: вызов, кэш, шаблон, эпилог - что происходит до шаблона у любого компонента
- Шаблон чужого компонента: копия, доработка результата, эпилог - куда копировать шаблон меню
- Путь запроса на витрине: пролог, компоненты, буфер, эпилог - когда меню собирается на странице
- Вывод разделов: дерево, подразделы, адреса - источник динамических пунктов