Введение
При разработке на коробочной версии Битрикс24 часто возникает необходимость программно получить список всех подразделов (подкатегорий) определённого раздела инфоблока. В отличие от простой фильтрации по SECTION_ID, древовидная структура инфоблоков требует использования полей LEFT_MARGIN и RIGHT_MARGIN для получения всех вложенных элементов независимо от глубины вложенности. Основным инструментом для этого является метод CIBlockSection::GetList в связке с навигацией по дереву.
Важное отличие: фильтр по SECTION_ID vs навигация по LEFT_MARGIN / RIGHT_MARGIN
Ключевая особенность: простая фильтрация по SECTION_ID возвращает только прямых потомков раздела (первый уровень вложенности). Для получения всех подразделов (на любую глубину) необходимо использовать поля LEFT_MARGIN и RIGHT_MARGIN, которые реализуют механизм Nested Sets (вложенные множества).
| Подход | Что возвращает | Глубина |
|---|---|---|
SECTION_ID = $parentId
|
Только прямые подразделы | 1 уровень |
LEFT_MARGIN > parent.LEFT_MARGIN и RIGHT_MARGIN < parent.RIGHT_MARGIN
|
Все подразделы на любой глубине | Все уровни |
Область применения и ключевые сущности
Метод CIBlockSection::GetList с фильтрацией по LEFT_MARGIN и RIGHT_MARGIN используется для получения иерархической структуры разделов инфоблока. Понимание этой связки необходимо при:
- Построении многоуровневых меню — вывод всех категорий с учётом вложенности
- Экспорте каталога — получение полной структуры категорий товаров
- Обработке всех элементов раздела — рекурсивный обход всех подразделов
- Миграции данных — копирование структуры разделов
- SEO-генерации — создание ЧПУ для всех уровней вложенности
- Фильтрации товаров — показ товаров из раздела и всех его подразделов
Ключевые сущности:
| Таблица / Класс | Описание |
|---|---|
b_iblock_section
|
Таблица с разделами инфоблоков |
CIBlockSection
|
Класс для работы с разделами |
LEFT_MARGIN
|
Левая граница раздела в дереве (Nested Sets) |
RIGHT_MARGIN
|
Правая граница раздела в дереве (Nested Sets) |
DEPTH_LEVEL
|
Уровень вложенности (1 — корень, 2 — первый подраздел и т.д.) |
Параметры метода CIBlockSection::GetList
Метод имеет следующую сигнатуру:
phppublic static function GetList($arOrder = [], $arFilter = [], $bIncCnt = false, $arSelect = [])
Параметр $arOrder (сортировка)
Определяет порядок сортировки разделов:
php$arOrder = ['left_margin' => 'ASC']; // сортировка по левой границе (древовидный порядок)
Доступные ключи: ID, NAME, SORT, LEFT_MARGIN, RIGHT_MARGIN, DEPTH_LEVEL, TIMESTAMP_X и др.
Параметр $arFilter (фильтрация)
Для получения всех подразделов используются следующие ключи:
| Ключ | Описание | Пример |
|---|---|---|
IBLOCK_ID
|
ID инфоблока |
$arParentSection['IBLOCK_ID']
|
>LEFT_MARGIN
|
Левая граница больше родительской |
$arParentSection['LEFT_MARGIN']
|
<RIGHT_MARGIN
|
Правая граница меньше родительской |
$arParentSection['RIGHT_MARGIN']
|
>DEPTH_LEVEL
|
Глубина больше родительской |
$arParentSection['DEPTH_LEVEL']
|
SECTION_ID
|
ID родительского раздела (только прямые потомки) |
$parentId
|
Практические примеры
Пример 1. Базовое получение всех подразделов (как в вашем коде)
phpif (\Bitrix\Main\Loader::includeModule('iblock')) {
// Получаем родительский раздел с ID = 4
$rsParentSection = CIBlockSection::GetByID(4);
if ($arParentSection = $rsParentSection->GetNext()) {
// Фильтр для получения всех подразделов на любую глубину
$arFilter = [
'IBLOCK_ID' => $arParentSection['IBLOCK_ID'],
'>LEFT_MARGIN' => $arParentSection['~LEFT_MARGIN'],
'<RIGHT_MARGIN' => $arParentSection['~RIGHT_MARGIN'],
'>DEPTH_LEVEL' => $arParentSection['DEPTH_LEVEL']
];
$rsSect = CIBlockSection::GetList(['left_margin' => 'ASC'], $arFilter);
while ($arSect = $rsSect->GetNext()) {
// Выводим ID каждого подраздела
echo "<pre>"; print_r($arSect['ID']); echo "</pre>";
}
}
}
Пример 2. Функция для получения всех подразделов с данными
php/**
* Получает все подразделы раздела инфоблока (рекурсивно, на любую глубину)
*
* @param int $sectionId ID родительского раздела
* @return array Массив подразделов с полной информацией
*/
function getAllSubsections(int $sectionId): array
{
if (!\Bitrix\Main\Loader::includeModule('iblock')) {
return [];
}
$subsections = [];
// Получаем родительский раздел
$rsParent = CIBlockSection::GetByID($sectionId);
$parent = $rsParent->Fetch();
if (!$parent) {
return [];
}
// Фильтр для всех подразделов (любой глубины)
$filter = [
'IBLOCK_ID' => $parent['IBLOCK_ID'],
'>LEFT_MARGIN' => $parent['LEFT_MARGIN'],
'<RIGHT_MARGIN' => $parent['RIGHT_MARGIN'],
'>DEPTH_LEVEL' => $parent['DEPTH_LEVEL']
];
$rsSections = CIBlockSection::GetList(
['LEFT_MARGIN' => 'ASC'],
$filter,
false,
['ID', 'NAME', 'CODE', 'DEPTH_LEVEL', 'LEFT_MARGIN', 'RIGHT_MARGIN', 'SECTION_PAGE_URL']
);
while ($section = $rsSections->Fetch()) {
$subsections[] = [
'ID' => $section['ID'],
'NAME' => $section['NAME'],
'CODE' => $section['CODE'],
'DEPTH_LEVEL' => $section['DEPTH_LEVEL'],
'URL' => $section['SECTION_PAGE_URL']
];
}
return $subsections;
}
// Использование
$subsections = getAllSubsections(4);
echo '<pre>'; print_r($subsections); echo '</pre>';
Пример 3. Сравнение: SECTION_ID vs LEFT_MARGIN/RIGHT_MARGIN
php$parentSectionId = 4;
// ❌ Только прямые подразделы (первый уровень)
$filterDirect = ['IBLOCK_ID' => 1, 'SECTION_ID' => $parentSectionId];
$rsDirect = CIBlockSection::GetList(['SORT' => 'ASC'], $filterDirect);
echo "Прямые подразделы (SECTION_ID):\n";
while ($sect = $rsDirect->Fetch()) {
echo "ID: {$sect['ID']}, NAME: {$sect['NAME']}, DEPTH: {$sect['DEPTH_LEVEL']}\n";
}
// ✅ Все подразделы на любую глубину (через LEFT_MARGIN/RIGHT_MARGIN)
$rsParent = CIBlockSection::GetByID($parentSectionId);
$parent = $rsParent->Fetch();
if ($parent) {
$filterAll = [
'IBLOCK_ID' => $parent['IBLOCK_ID'],
'>LEFT_MARGIN' => $parent['LEFT_MARGIN'],
'<RIGHT_MARGIN' => $parent['RIGHT_MARGIN'],
'>DEPTH_LEVEL' => $parent['DEPTH_LEVEL']
];
$rsAll = CIBlockSection::GetList(['LEFT_MARGIN' => 'ASC'], $filterAll);
echo "\nВсе подразделы (LEFT_MARGIN/RIGHT_MARGIN):\n";
while ($sect = $rsAll->Fetch()) {
$indent = str_repeat('--', $sect['DEPTH_LEVEL'] - $parent['DEPTH_LEVEL']);
echo "{$indent} ID: {$sect['ID']}, NAME: {$sect['NAME']}, DEPTH: {$sect['DEPTH_LEVEL']}\n";
}
}
Пример 4. Получение подразделов с построением дерева
phpfunction getSubsectionsTree(int $sectionId): array
{
if (!\Bitrix\Main\Loader::includeModule('iblock')) {
return [];
}
$rsParent = CIBlockSection::GetByID($sectionId);
$parent = $rsParent->Fetch();
if (!$parent) {
return [];
}
$filter = [
'IBLOCK_ID' => $parent['IBLOCK_ID'],
'>LEFT_MARGIN' => $parent['LEFT_MARGIN'],
'<RIGHT_MARGIN' => $parent['RIGHT_MARGIN']
];
$rsSections = CIBlockSection::GetList(
['LEFT_MARGIN' => 'ASC'],
$filter,
false,
['ID', 'NAME', 'DEPTH_LEVEL', 'LEFT_MARGIN', 'RIGHT_MARGIN']
);
// Строим дерево
$tree = [];
$stack = [];
$currentDepth = $parent['DEPTH_LEVEL'];
while ($section = $rsSections->Fetch()) {
$level = $section['DEPTH_LEVEL'];
$node = [
'ID' => $section['ID'],
'NAME' => $section['NAME'],
'CHILDREN' => []
];
if ($level == $currentDepth + 1) {
// Прямой потомок текущего узла
$stack[] = &$node;
} else {
// Возвращаемся на уровень выше
while (count($stack) > 0 && $stack[count($stack) - 1]['DEPTH_LEVEL'] >= $level) {
array_pop($stack);
}
if (count($stack) > 0) {
$stack[count($stack) - 1]['CHILDREN'][] = &$node;
}
$stack[] = &$node;
}
$node['DEPTH_LEVEL'] = $level;
if (count($stack) == 1) {
$tree[] = &$node;
}
}
return $tree;
}
Пример 5. Получение ID всех подразделов (включая вложенные)
phpfunction getAllSubsectionIds(int $sectionId): array
{
if (!\Bitrix\Main\Loader::includeModule('iblock')) {
return [];
}
$rsParent = CIBlockSection::GetByID($sectionId);
$parent = $rsParent->Fetch();
if (!$parent) {
return [];
}
$filter = [
'IBLOCK_ID' => $parent['IBLOCK_ID'],
'>LEFT_MARGIN' => $parent['LEFT_MARGIN'],
'<RIGHT_MARGIN' => $parent['RIGHT_MARGIN']
];
$ids = [];
$rsSections = CIBlockSection::GetList(['LEFT_MARGIN' => 'ASC'], $filter, false, ['ID']);
while ($section = $rsSections->Fetch()) {
$ids[] = $section['ID'];
}
return $ids;
}
// Использование: получим ID всех подразделов раздела 4
$allSubIds = getAllSubsectionIds(4);
echo "ID всех подразделов: " . implode(', ', $allSubIds);
Пример 6. Проверка, является ли раздел родительским для другого
phpfunction isParentSection(int $parentId, int $childId): bool
{
if (!\Bitrix\Main\Loader::includeModule('iblock')) {
return false;
}
$rsParent = CIBlockSection::GetByID($parentId);
$parent = $rsParent->Fetch();
$rsChild = CIBlockSection::GetByID($childId);
$child = $rsChild->Fetch();
if (!$parent || !$child) {
return false;
}
// Проверяем, что разделы в одном инфоблоке
if ($parent['IBLOCK_ID'] != $child['IBLOCK_ID']) {
return false;
}
// Дочерний раздел находится внутри границ родительского
return ($parent['LEFT_MARGIN'] < $child['LEFT_MARGIN'] &&
$parent['RIGHT_MARGIN'] > $child['RIGHT_MARGIN']);
}
// Использование
if (isParentSection(4, 10)) {
echo "Раздел 10 является подразделом раздела 4";
}
Пример 7. Получение элементов из раздела со всеми подразделами
phpfunction getElementsFromSectionWithSubsections(int $sectionId): array
{
if (!\Bitrix\Main\Loader::includeModule('iblock')) {
return [];
}
// Получаем ID всех подразделов
$subsectionIds = getAllSubsectionIds($sectionId);
// Добавляем сам родительский раздел
$allSectionIds = array_merge([$sectionId], $subsectionIds);
// Получаем элементы из всех этих разделов
$filter = [
'IBLOCK_ID' => 1,
'SECTION_ID' => $allSectionIds,
'INCLUDE_SUBSECTIONS' => 'Y' // Важно! Включает элементы из подразделов
];
$elements = [];
$rsElements = CIBlockElement::GetList(
['SORT' => 'ASC'],
$filter,
false,
false,
['ID', 'NAME', 'IBLOCK_SECTION_ID']
);
while ($element = $rsElements->Fetch()) {
$elements[] = $element;
}
return $elements;
}
Структура таблиц и принцип Nested Sets
sql-- b_iblock_section (разделы) -- ID, IBLOCK_ID, NAME, DEPTH_LEVEL, LEFT_MARGIN, RIGHT_MARGIN -- Принцип работы LEFT_MARGIN / RIGHT_MARGIN: -- Корневой раздел: LEFT_MARGIN = 1, RIGHT_MARGIN = 10 -- Подраздел 1: LEFT_MARGIN = 2, RIGHT_MARGIN = 5 -- Подподраздел: LEFT_MARGIN = 3, RIGHT_MARGIN = 4 -- Подраздел 2: LEFT_MARGIN = 6, RIGHT_MARGIN = 9 -- Подподраздел: LEFT_MARGIN = 7, RIGHT_MARGIN = 8
Альтернативные методы
| Метод | Описание |
|---|---|
CIBlockSection::GetByID()
|
Получение одного раздела по ID |
CIBlockSection::GetTreeList()
|
Получение дерева разделов (устаревший) |
CIBlockSection::GetNavChain()
|
Получение цепочки навигации (хлебные крошки) |
CIBlockSection::GetSectionElementsCount()
|
Получение количества элементов в разделе |
Особенности и рекомендации
-
Обязательное подключение модуля: Перед вызовом методов необходимо подключить модуль
iblockчерезCModule::IncludeModule('iblock'). -
Использование
~LEFT_MARGINvsLEFT_MARGIN: В вашем коде используется$arParentSection['~LEFT_MARGIN'](с тильдой). Тильда означает получение значения в исходном формате (без HTML-экранирования). Для числовых полей это не критично, но хорошая практика. -
Важность сортировки по
LEFT_MARGIN: При получении дерева разделов всегда сортируйте поLEFT_MARGIN ASC— это гарантирует правильный порядок обхода. -
Фильтр
>DEPTH_LEVEL: Используйте этот фильтр, если хотите исключить сам родительский раздел из выборки. -
Производительность: Запросы с
LEFT_MARGINиRIGHT_MARGINочень быстрые, так как эти поля индексированы. Это гораздо эффективнее рекурсивных запросов. -
Кеширование: При частых запросах структуры разделов используйте кеширование, например, через
CIBlockSection::GetList([...], [...], false, false, ['CACHE_TIME' => 3600]). -
Глубина вложенности: Теоретически не ограничена, но практически в Битрикс редко превышает 10 уровней.
Заключение
CIBlockSection::GetList с фильтрацией по LEFT_MARGIN и RIGHT_MARGIN — это основной и наиболее эффективный способ получения всех подразделов раздела инфоблока в коробочной версии Битрикс24. В отличие от простой фильтрации по SECTION_ID, которая возвращает только прямых потомков, навигация по Nested Sets позволяет получить все вложенные подразделы на любую глубину за один запрос. Понимание этого подхода необходимо каждому разработчику, который работает с иерархическими структурами: каталогами товаров, меню, древовидными справочниками и другими многоуровневыми данными.
