Не буду писать про боль при интегации дефотного шаблона. Давайте просто напишем свой, с блекджеком и вот этим всем.

В заметке (интересного):

  • построение древовидного массива без рекурсии (неограниченной вложенности)
  • анонимная рекурсивная функция
  • формирование урлов для секций супер-быстрым способом
  • поработаем с сущностями инфоблока через ORM D7

Вводные данные

Предположим, что шаблон вашего сайта называется main. Все пути в заметке будут на основе его.

Шаблон меню обзовём template-with-ul-recursive.

Тип меню будет catalog-left-menu

Вызываем компонент bitrix:menu

<?php 
$APPLICATION->IncludeComponent(
    "bitrix:menu",
    "template-with-ul-recursive",
    array(
        "ROOT_MENU_TYPE" => "catalog-left-menu",
        "MENU_CACHE_TYPE" => "N",
        "MENU_CACHE_TIME" => "3600",
        "MENU_CACHE_USE_GROUPS" => "N",
        "MENU_CACHE_GET_VARS" => array(
        ),
        "MAX_LEVEL" => "4",
        "CHILD_MENU_TYPE" => "",
        "USE_EXT" => "Y",
        "DELAY" => "N",
        "ALLOW_MULTI_SELECT" => "Y",
        "CSS_CLASS_OUTER" => "left-menu",
        "COMPONENT_TEMPLATE" => "template-with-ul-recursive"
    ),
    false
);
?>

Из важного:

  • MENU_CACHE_TYPE в N - кеширование обязательно выключаем, у нас будет своё
  • USE_EXT в Y - пункты меню будем наполнять в .ext файле
  • ALLOW_MULTI_SELECT в Y - корректно определит автивности всех вложенностей

Остальные параметры индивидуальны.

CSS-класс для каждого пункта меню

Для секции добавим (пригодится) новое пользовательское свойство с именем UF_CSS_CLASS_MENU и типом строка. Свойство можно добавить на странице редактирования любого раздела нужного инфоблока (вкладка "Доп. поля")

Наполняем пунктами меню

В корне сайта создаём файл с именем .catalog-left-menu.menu_ext.php и содержимым:

if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
 
/** @var array $aMenuLinks */
 
use Bitrix\Iblock\SectionTable;
use Bitrix\Main\Loader;
use Bitrix\Iblock\IblockTable;
use Bitrix\Main\Data\Cache;
use Bitrix\Main\Data\TaggedCache;
use Bitrix\Main\Entity;
use Bitrix\Main\Application;
 
$catalogIblockId = 1;
 
$aMenuLinksExt = array();
 
$cache = Cache::createInstance();
$cacheTime = 86400;
$cacheId = 'catalog-left-menu-ext';
 
if ($cache->initCache($cacheTime, $cacheId, '/olegpro/bitrix.menu_ext/catalog-left-menu')) {
    $aMenuLinksExt = $cache->GetVars();
} elseif ($cache->startDataCache()) {
    if (Loader::includeModule('iblock')) {
        $iblockIterator = IblockTable::getList(array(
            'select' => array('SECTION_PAGE_URL', 'CODE'),
            'filter' => array('=ID' => $catalogIblockId),
            'limit' => 1
        ));
 
        if ($iblock = $iblockIterator->fetch()) {
 
            $connection = Application::getConnection();
 
            $entityUtsTableName = sprintf('b_uts_iblock_%s_section', $catalogIblockId);
 
            $entityUtsTableNameTableExists = $connection->isTableExists($entityUtsTableName);
 
            if ($entityUtsTableNameTableExists) {
                $entityUts = Entity\Base::compileEntity('utsSectionOlegproLeftCatalogMenu' . randString(4),
                    [
                        'VALUE_ID' => ['data_type' => 'integer'],
                        'UF_CSS_CLASS_MENU' => ['data_type' => 'string'],
                    ],
                    ['table_name' => sprintf('b_uts_iblock_%s_section', $catalogIblockId)]
                );
            }
 
            $sectionIteratorParameters = array(
                'select' => [
                    'CODE',
                    'NAME',
                    'ID',
                    'DEPTH_LEVEL',
                    'IBLOCK_SECTION_ID',
                ],
                'filter' => [
                    '=IBLOCK_ID' => $catalogIblockId,
                    '<=DEPTH_LEVEL' => 4,
                    '=ACTIVE' => 'Y',
                    '=GLOBAL_ACTIVE' => 'Y',
                ],
                'order' => [
                    'LEFT_MARGIN' => 'ASC',
                ],
                'runtime' => [],
            );
 
            if ($entityUtsTableNameTableExists && isset($entityUts) && is_object($entityUts)) {
                $sectionIteratorParameters['select']['UF_CSS_CLASS_MENU'] = 'UF.UF_CSS_CLASS_MENU';
 
                $sectionIteratorParameters['runtime'][] = new Entity\ReferenceField('UF',
                    $entityUts,
                    ['=this.ID' => 'ref.VALUE_ID']
                );
            }
 
            $sectionIterator = SectionTable::getList($sectionIteratorParameters);
 
            $sections = [];
 
            while ($section = $sectionIterator->fetch()) {
                $sections[$section['ID']] = $section;
            }
 
            unset($section);
 
            foreach ($sections as $section) {
 
                $sectionCodes = [
                    $section['CODE']
                ];
 
                $parentId = $section['IBLOCK_SECTION_ID'];
 
                while (isset($parentId)) {
 
                    if (isset($sections[$parentId])) {
                        $sectionCodes[] = $sections[$parentId]['CODE'];
 
                        $parentId = $sections[$parentId]['IBLOCK_SECTION_ID'];
                    } else {
                        $parentId = null;
                    }
 
                }
 
                $aMenuLinksExt[] = array(
                    $section['NAME'],
                    str_replace(
                        array(
                            '#SITE_DIR#',
                            '#IBLOCK_CODE#',
                            '#SECTION_CODE_PATH#',
                        ),
                        array(
                            SITE_DIR,
                            $iblock['CODE'],
                            implode('/', array_reverse($sectionCodes))
                        ),
                        $iblock['SECTION_PAGE_URL']
                    ),
                    array(),
                    array(
                        'ID' => $section['ID'],
                        'DEPTH_LEVEL' => $section['DEPTH_LEVEL'],
                        'CODE' => $section['CODE'],
                        'IBLOCK_SECTION_ID' => $section['IBLOCK_SECTION_ID'],
                        'UF_CSS_CLASS_MENU' => $section['UF_CSS_CLASS_MENU'],
                    )
                );
            }
 
            if (defined('BX_COMP_MANAGED_CACHE')) {
                $tagCache = new TaggedCache();
                $tagCache->startTagCache('/olegpro/bitrix.menu_ext/catalog-left-menu');
                $tagCache->registerTag(sprintf('iblock_id_%s', $catalogIblockId));
                $tagCache->endTagCache();
            }
 
        } else {
            $cache->abortDataCache();
        }
    }
    $cache->endDataCache($aMenuLinksExt);
}
 
$aMenuLinks = array_merge($aMenuLinks, $aMenuLinksExt);

Значение переменной $catalogIblockId меняем на ID инфоблока каталога.

Важно! Урлы секций обрабатываются только для варианта #SECTION_CODE_PATH#

Создаём папку и файлы шаблона

Создадим папку для нашего шаблона local/templates/main/components/bitrix/menu/template-with-ul-recursive

Создаём в этой папке файл result_modifier.php. В нём мы соберём массив нужного нам формата, чтобы потом в шаблоне было удобно выводить само меню.

<?php
/**
 * Created by olegpro.ru.
 * User: Oleg Maksimenko <oleg.39style@gmail.com>
 * Date: 26.08.2016. Time: 11:15
 */
 
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
 
/** @var array $arResult */
/** @var array $arParams */
/** @var CBitrixComponentTemplate $this */
 
 
// [section_id => [all parents ids]]
$selectedItems = [];
 
// [section_id => parent_section_id]
$mapSectionIds = [];
 
// [section_id => [all parents ids]]
$mapSectionParentIds = [];
 
$allSelectedItems = [];
 
foreach ($arResult as $arItem) {
    if ($arItem['SELECTED'] && isset($arItem['PARAMS']['ID'])) {
        $selectedItems[$arItem['PARAMS']['ID']] = 1;
    }
 
    if (isset($arItem['PARAMS']['ID']) && array_key_exists('IBLOCK_SECTION_ID', $arItem['PARAMS'])) {
        $mapSectionIds[$arItem['PARAMS']['ID']] = $arItem['PARAMS']['IBLOCK_SECTION_ID'];
    }
}
 
$selectedDirectItemsId = $selectedItems;
 
foreach ($mapSectionIds as $sectionId => $sectionParentId) {
 
    $sectionsParent = [];
 
    $parentId = $sectionParentId;
 
    while (isset($parentId)) {
 
        $sectionsParent[] = $parentId;
 
        $parentId = isset($mapSectionIds[$parentId])
            ? $mapSectionIds[$parentId]
            : null;
    }
 
    $mapSectionParentIds[$sectionId] = $sectionsParent;
 
    if (isset($selectedItems[$sectionId])) {
        $selectedItems[$sectionId] = $sectionsParent;
 
        $allSelectedItems = array_merge($allSelectedItems, $sectionsParent);
    }
 
}
 
 
if (!empty($allSelectedItems)) {
    $allSelectedItems = array_flip($allSelectedItems);
}
 
 
foreach ($arResult as $key => $arItem) {
    if (
    isset(
        $arItem['PARAMS']['ID'],
        $allSelectedItems[$arItem['PARAMS']['ID']]
    )
    ) {
        $arItem['SELECTED'] = true;
 
        $arResult[$key] = $arItem;
    }
}
 
unset($key, $arItem);
 
 
// Generate hierarchical tree
$map = [
    0 => [
        'CHILDREN' => []
    ]
];
 
foreach ($arResult as &$arItem) {
    $arItem['CHILDREN'] = [];
 
    $map[$arItem['PARAMS']['ID']] = &$arItem;
}
 
 
foreach ($arResult as &$arItem) {
    $map[(int)$arItem['PARAMS']['IBLOCK_SECTION_ID']]['CHILDREN'][] = &$arItem;
}
 
$arResultCopy = $arResult;
 
$arResult = [
    'CHILDREN' => $map[0]['CHILDREN'],
    'SELECTED_DIRECT_IDS' => $selectedDirectItemsId,
];
 
$map = null;
 
unset($map);

Создаём в папке local/templates/main/components/bitrix/menu/template-with-ul-recursive файл с параметрами .parameters.php

<?php
 
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
 
use Bitrix\Main\Localization\Loc;
 
Loc::loadMessages(__FILE__);
 
$arTemplateParameters = array(
    'CSS_CLASS_OUTER' => array(
        'NAME' => Loc::getMessage('MENU_CSS_CLASS_OUTER'),
        'TYPE' => 'STRING',
        'DEFAULT' => 'menu',
    ),
    'CSS_CLASS_ITEM' => array(
        'NAME' => Loc::getMessage('MENU_CSS_CLASS_ITEM'),
        'TYPE' => 'STRING',
        'DEFAULT' => 'menu',
    ),
    'CSS_CLASS_ITEM_ACTIVE' => array(
        'NAME' => Loc::getMessage('MENU_CSS_CLASS_ITEM_ACTIVE'),
        'TYPE' => 'STRING',
        'DEFAULT' => 'menu',
    ),
);

Создаём в папке local/templates/main/components/bitrix/menu/template-with-ul-recursive/lang/ru/ файл .parameters.php с языковыми фразами параметров

<?php
/**
 * Created by olegpro.ru
 * User: Oleg Maksimenko <oleg.39style@gmail.com>
 * Date: 16.11.2015. Time: 15:05
 */
 
$MESS['MENU_CSS_CLASS_OUTER'] = 'CSS класс для списка (ul)';
$MESS['MENU_CSS_CLASS_ITEM'] = 'CSS класс для элемента списка (li)';
$MESS['MENU_CSS_CLASS_ITEM_ACTIVE'] = 'CSS класс для активного элемента списка';

Создаём в папке local/templates/main/components/bitrix/menu/template-with-ul-recursive/ файл template.php с нашим шаблоном

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
 
/** @var array $arResult */
/** @var array $arParams */
/** @var CBitrixComponentTemplate $this */
 
$this->setFrameMode(true);
 
if (!empty($arResult)) {
 
    $menuRecursive = function ($items, $level = 1) use ($arParams, $arResult, &$menuRecursive) {
 
        $items = array_values($items);
 
        $countItems = sizeof($items);
 
        if (sizeof($items) > 0) {
 
            $cssClassOuter = array_filter(
                array_map('trim', explode(' ', $arParams['CSS_CLASS_OUTER']))
            );
 
            $cssClassOuter = array_map(
                function ($value) use ($level, &$cssClassOuter) {
                    return implode(' ', [$value, sprintf('%s-level%d', $value, $level)]);
                },
                $cssClassOuter
            );
 
            ?><ul class="<?php echo implode(' ', $cssClassOuter) ?>"><?php
 
                foreach ($items as $i => $arItem) {
                    $cssClasses = [
                        $arParams['CSS_CLASS_ITEM'],
                    ];
 
                    if (($i + 1) % 2 == 0) {
                        $cssClasses[] = 'even';
                    } else {
                        $cssClasses[] = 'odd';
                    }
 
                    if ($i == 0) {
                        $cssClasses[] = 'first';
                    }
 
                    if ($countItems == $i + 1) {
                        $cssClasses[] = 'last';
                    }
 
                    if ($arItem['SELECTED']) {
                        $cssClasses[] = $arParams['CSS_CLASS_ITEM_ACTIVE'];
                    }
 
                    if (isset($arItem['PARAMS'], $arItem['PARAMS']['CLASS']) && strlen(trim($arItem['PARAMS']['CLASS']))) {
                        $cssClasses[] = trim($arItem['PARAMS']['CLASS']);
                    }
 
                    if ($arItem['SELECTED'] && isset($arResult['SELECTED_DIRECT_IDS'][$arItem['PARAMS']['ID']])) {
                        ?><li class="<?php echo implode(' ', $cssClasses) ?>">
                            <span><?php echo $arItem['TEXT'] ?></span>
                            <?php $menuRecursive($arItem['CHILDREN'], $arItem['PARAMS']['DEPTH_LEVEL'] + 1) ?>
                        </li><?php
                    } else {
                        ?><li class="<?php echo implode(' ', $cssClasses) ?>">
                            <a href="<?php echo $arItem['LINK'] ?>"><?php echo $arItem['TEXT'] ?></a>
                            <?php $menuRecursive($arItem['CHILDREN'], $arItem['PARAMS']['DEPTH_LEVEL'] + 1) ?>
                        </li><?php
                    }
                }
            ?></ul><?php
 
        }
 
    };
 
    $menuRecursive($arResult['CHILDREN']);
 
}

Вот, собственно, и всё. Надеюсь, вам понравилось Подписывайтесь на канал, ставьте пальцы вверх и т.п. :)