Перейти к содержимому

Меню изнутри - файлы, типы, сборка пунктов, кэш

Разбираем меню по слоям: где физически лежат пункты, как компонент обходит дерево разделов и что от этого обхода попадает в кэш. Дальше смотрим, каким меню приходит в шаблон и чем оно отличается от хлебных крошек.

Механика

Пункты меню лежат в файлах внутри папок разделов, а не в таблицах базы данных. Имя файла собрано из точки, типа меню и суффикса: файл .left.menu.php описывает меню типа left. Тип меню - это идентификатор латиницей, и с местом вывода на странице он никак не связан.

Сам тип регистрируется в настройках модуля управления структурой, отдельно для каждого сайта. Компонент читает файл по имени и работает даже с незарегистрированным типом, а вот интерфейс административной части показывает только заведённые типы меню.

Один пункт статического меню - это массив ровно из пяти элементов с закреплённым смыслом. По порядку: название, адрес, массив дополнительных адресов для подсветки, массив произвольных параметров и условие показа. Последний элемент - строка кода на PHP, которая должна вернуть истину.

Файл расширения .тип.menu_ext.php отличается от обычного моментом и способом работы. Он не хранит готовые пункты, а исполняется компонентом во время сборки меню и дописывает свои пункты в тот же массив. Компонент читает такой файл только при включённом параметре подключения расширений.

Поиск файла меню идёт снизу вверх: текущий раздел, родительский, дальше до корня сайта. Обход останавливается на первом найденном файле независимо от его содержимого, поэтому меню раздела без своего файла - это меню родителя. Статический файл заменяет только статический, а файл расширения - только расширение.

Активность пункта компонент определяет сравнением адресов, а не структурой разделов. Совпал адрес пункта с адресом текущей страницы - в поле SELECTED приходит истина. Третий элемент массива добавляет адреса, при которых пункт считается активным тоже: так подсвечивают раздел на страницах его элементов.

В кэш меню попадает и собранный список пунктов, и готовая разметка шаблона. Ключ кэша учитывает адрес страницы, тип меню, группы посетителя и значимые переменные запроса, поэтому вариантов накапливается примерно столько же, сколько страниц. На попадании в кэш шаблон повторно не выполняется вовсе.

Шаблону компонент отдаёт плоский массив $arResult, а не вложенное дерево. Вложенность передаётся полем DEPTH_LEVEL каждого пункта, и разметку с вложенными списками шаблон строит сам. Рядом с полями уровня приходят TEXT, LINK, SELECTED, ITEM_TYPE, PARAMS и PERMISSION.

Своя разметка живёт в копии шаблона внутри шаблона сайта, а правка данных - в файле result_modifier.php рядом с ней. Первый отвечает за представление, второй меняет $arResult до вывода и попадает в тот же кэш. Штатный шаблон компонента при этом остаётся нетронутым и обновляемым.

Хлебные крошки собираются из другого источника и файлов меню не читают вовсе. Цепочку наполняет платформа заголовками разделов пути и вызовами из кода страницы, а компонент только рисует накопленное. Поэтому меню и цепочка на одной странице расходятся легко и совершенно законно.

Шаги

  1. Посмотреть тип меню в параметрах компонента и найти файлы этого типа в дереве разделов.
  2. Пройти от текущего раздела вверх до корня и определить, какой файл выигрывает поиск.
  3. Открыть выигравший файл и разобрать его пункты по пяти элементам массива.
  4. Проверить, подключается ли файл расширения и что именно он дописывает в массив пунктов.
  5. Посмотреть, каким массив пришёл в шаблон, и только после этого менять разметку.

Код

Ищем файлы меню в дереве разделов:

Окно терминала
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/ и ниже

Файл в разделе перекрывает родительский полностью, поэтому список найденных путей отвечает на вопрос о наследовании быстрее любого чтения кода. Пустой файл в списке выглядит так же, как заполненный.

Разбираем пункт статического меню:

/catalog/.left.menu.php
$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_LEVEL
foreach ($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.php
foreach ($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?

Первый хранит готовый массив пунктов, второй собирает пункты кодом во время сборки меню. Второй читается только при включённом параметре подключения расширений.

Как заставить компонент брать меню из корня, а не из раздела?

Никак: обход идёт вверх и останавливается на первом найденном файле. Лишние файлы меню в разделах удаляют, и тогда компонент доходит до корня.

Откуда меню берёт вложенные уровни?

Из файлов меню вложенных разделов того типа, который задан для дочерних уровней. Сам файл раздела уровней не содержит: это плоский список пунктов.

Можно ли добавить пункт в административное меню файлом меню?

Файл меню админки лежит в каталоге ядра, и правка теряется при обновлении. Свои пункты административного меню добавляют обработчиком события из своего модуля.

Смежное

Первоисточник