Play
online · 30 мин
Туториалы 23 мин чтения 17.09.2026

Инфоблоки Битрикс: создание, вывод элементов, разделы и свойства

Инфоблоки в Битриксе — это не просто таблица с товарами, а связка из четырёх уровней, от которой зависит скорость выборки и стабильность обмена с 1С. Разбираем, как создать инфоблок, вывести элементы и грамотно работать со свойствами.

Инфоблоки Битрикс: что это и из чего состоит

Когда к нам приходят с проектом, где каталог перевалил за 20 000 SKU, первый вопрос не «как вывести товары», а «как устроено хранение». Инфоблоки в Битриксе — это не просто «таблица с товарами», а связка из четырёх уровней. От того, насколько правильно спроектирована эта структура, зависит всё: скорость выборки, удобство работы контент-менеджеров, стабильность обмена с 1С и предсказуемость SEO-полей.

В нашей практике инфоблоки закрывают две большие задачи. Первая — e-commerce: торговый каталог, торговые предложения (SKU), бренды, коллекции, характеристики. Вторая — контент: новости, статьи, акции, баннеры, вакансии, отзывы. Оба сценария используют один и тот же движок, но нагрузка и требования к структуре у них разные. У магазина с активным импортом из 1С критично, чтобы свойства не дублировались и не терялись при обновлении. У новостного раздела — чтобы разделы и привязки работали без лишних запросов.

Правильно заложенная структура даёт конкретный результат: предсказуемые запросы через CIBlockElement::GetList и CIBlockSection::GetList, отсутствие «чужих» элементов в выборке, корректную работу ЧПУ и пагинации, а также возможность без боли подключить D7 ORM там, где это оправдано. Если структура «налеплена» на ходу — мета-теги затираются импортом, разделы путаются с элементами, а фильтры по свойствам валят страницу. Дальше разберём, из чего инфоблок состоит и как эти сущности связаны в базе.

С точки зрения архитектуры инфоблок — это четыре уровня вложенности плюс свойства.

Тип инфоблока — верхний уровень, «категория категорий». Например, тип catalog для торговых каталогов или news для новостей. Тип задаёт набор общих настроек и позволяет группировать инфоблоки с одинаковой логикой. У одного типа может быть много инфоблоков — скажем, «Каталог Москвы» и «Каталог регионов».

Инфоблок — конкретная сущность внутри типа: «Товары», «Новости компании», «Акции». У него есть собственные настройки — права доступа, привязка к сайту, шаблоны URL, включённость версионности и SKU. Именно на уровне инфоблока задаётся «Символьный код API» — строка от 1 до 50 символов, без которой не заработает D7 ORM.

Раздел — узел иерархии внутри инфоблока. Разделы могут вкладываться друг в друга (каталог → категория → подкатегория), у каждого есть свои поля и наследуемые свойства. Элемент может лежать в нескольких разделах сразу — это штатное поведение, а не баг.

Элемент — конечная запись: товар, новость, баннер. Хранит основные поля (название, описание, активность, сортировку) и привязку к разделам через отдельную таблицу связей.

Свойство — пользовательский атрибут, привязанный к инфоблоку. Цвет, размер, бренд, автор, дата — всё это свойства. В БД они лежат в отдельных таблицах по типам значений, а не в основной таблице элементов. Именно поэтому выборка свойств требует явного указания IBLOCK_ID в фильтре и ID в arSelectFields — иначе CIBElement::GetProperties молча вернёт пустоту.

Сущность Класс API Таблица в MySQL За что отвечает
Тип инфоблока CIBlockType b_iblock_type Группировка инфоблоков по назначению
Инфоблок CIBlock b_iblock Настройки, права, привязка к сайту
Раздел CIBlockSection b_iblock_section Иерархия каталога, навигация
Элемент CIBlockElement b_iblock_element Записи: товары, новости, акции
Свойство CIBlockProperty b_iblock_property Пользовательские атрибуты элементов

Элементы инфоблока Битрикс: как вывести через CIBlockElement::GetList

Типичный сценарий: компонент на странице выводит каталог, но нужно отдать те же данные в другом месте — в XML-фиде для маркетплейса, в письме о новых поступлениях, в JSON-ответе для AJAX-подгрузки или в выгрузке на Яндекс.Маркет. Ставить ради этого компонент и разбирать его $arResult — лишний слой: компонент тянет кэш, шаблон, параметры ЧПУ, а нам нужны только сырые элементы. Именно здесь и всплывает CIBlockElement::GetList — классический метод, который в Bitrix 24 остаётся рабочим инструментом, когда данные надо получить напрямую, без обвязки.

У нас на проектах прямая выборка через API закрывает четыре типовые задачи: фиды и выгрузки для внешних площадок, письма и уведомления с товарными позициями, AJAX-запросы, где компонент избыточен, и служебные скрипты — сверки остатков, массовые обновления, отчёты. Результат — обычный массив. Его можно отдать в json_encode, подставить в шаблон письма, скормить генератору XML.

Ниже разберём базовый вызов, потом — почему в фильтре обязателен IBLOCK_ID и как работает навигация, а в конце — как вытащить пользовательские свойства и где на этом обычно спотыкаются.

PHP
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

\Bitrix\Main\Loader::includeModule('iblock');

$arOrder  = ['SORT' => 'ASC', 'NAME' => 'ASC'];
$arFilter = [
    'IBLOCK_ID' => 15,          // замените на ваш IBLOCK_ID
    'ACTIVE'    => 'Y',
    'SECTION_ID' => 42,
];
$arSelect = [
    'ID', 'IBLOCK_ID', 'NAME', 'CODE',
    'PREVIEW_TEXT', 'DETAIL_PAGE_URL', 'PROPERTY_AUTHOR',
];

$navParams = ['nPageSize' => 50, 'iNumPage' => 1];

$rsElements = CIBlockElement::GetList(
    $arOrder,
    $arFilter,
    false,
    $navParams,
    $arSelect
);

$items = [];
while ($arItem = $rsElements->GetNext()) {
    $items[] = [
        'ID'    => (int)$arItem['ID'],
        'NAME'  => $arItem['NAME'],
        'URL'   => $arItem['DETAIL_PAGE_URL'],
        'AUTHOR' => $arItem['PROPERTY_AUTHOR_VALUE'],
    ];
}

echo json_encode($items, JSON_UNESCAPED_UNICODE);

Ключевое здесь — порядок пяти аргументов: сортировка, фильтр, счётчик, навигация, поля выборки. Меняют обычно три вещи. Первое — arFilter: вместо SECTION_ID подставляют ACTIVE, >ID, PROPERTY_* под конкретную задачу. Второе — arSelect: чем меньше полей, тем легче запрос; если тянете только ID и NAME для выпадашки, не просите DETAIL_TEXT — он весит больше всего. Третье — NavStartParams: для фида на 10 000 позиций разбивайте на страницы, а не выбирайте всё разом.

IBLOCK_ID в фильтре обязателен — это не формальность. Без него ядро не может построить корректный запрос по таблицам свойств и либо вернёт пустоту, либо начнёт цеплять элементы из чужих инфоблоков: по опыту подрядчиков, отсутствие IBLOCK_ID в фильтре — частая причина и замедления, и «странных» данных в выборке.

Второй параметр bIncCnt (у нас false) — это флаг подсчёта элементов в разделе: если поставить true, в результат добавятся поля ELEMENT_CNT с количеством, но запрос станет тяжелее. Включайте его, только когда счётчик реально нужен в шаблоне, а не «на всякий случай». Третий параметр — NavStartParams: массив с ключами nPageSize (сколько элементов на страницу) и iNumPage (номер страницы). Для постраничной навигации в компонентах он связывается с $arResult['NAV_STRING'], а в скриптах достаточно передать оба ключа вручную. Если навигация не нужна — передавайте false, иначе ядро по умолчанию может ограничить выборку.

Как вывести пользовательские свойства элемента

Чтобы в выборке появились пользовательские свойства, их коды надо явно перечислить в arSelect в формате PROPERTY_КОД. Если код свойства — AUTHOR, в массиве полей пишем PROPERTY_AUTHOR. Тогда в результате появится плоское поле PROPERTY_AUTHOR_VALUE — именно к нему и обращаются в шаблоне. Это самый быстрый вариант: одно значение, без вложенных массивов.

Если же нужно вытащить сразу весь блок свойств (например, для универсального экспорта), вместо перечисления кодов в arSelect передают PROPERTIES — тогда ядро соберёт свойства в отдельный массив $arItem['PROPERTIES'], и к значениям обращаются как $arItem['PROPERTIES']['AUTHOR']['VALUE']. Такой способ удобен, когда список свойств заранее неизвестен или меняется. Важная деталь из документации: CIBlockElement::GetProperties не сработает, если в arSelectFields не указаны ID и IBLOCK_ID, а в arFilter - IBLOCK_ID. То есть даже при выборке «всех свойств» идентификаторы инфоблока и элемента в запросе должны быть на месте.

PHP
<?php
$rsElements = CIBlockElement::GetList(
    ['SORT' => 'ASC'],
    ['IBLOCK_ID' => 15, 'ACTIVE' => 'Y'],
    false,
    false,
    ['ID', 'IBLOCK_ID', 'NAME', 'PROPERTIES']
);

while ($arItem = $rsElements->GetNext()) {
    $author = $arItem['PROPERTIES']['AUTHOR']['VALUE'] ?? '';
    $photo  = $arItem['PROPERTIES']['PHOTO']['VALUE'] ?? 0;

    echo '<div class="slide">';
    echo '<h3>' . htmlspecialchars($arItem['NAME']) . '</h3>';
    echo '<p>Автор: ' . htmlspecialchars($author) . '</p>';
    echo '</div>';
}

Грабля с GetNext() и Fetch(). Многие по привычке берут Fetch(), а не GetNext() — и получают сюрприз: Fetch() возвращает «сырые» данные без постобработки. Поля приходят неэкранированными, а свойства — с префиксами в именах ключей вместо привычного PROPERTIES['AUTHOR']['VALUE']. На ru.stackoverflow разбирали случай, когда на этом ломалась вёрстка слайдера: значения свойств приходили в другом формате, и $arItem['PROPERTIES'] был пуст. Решение простое — для штатного вывода используйте GetNext(): он экранирует HTML и собирает свойства в ожидаемую структуру. Fetch() оставляйте только там, где нужны именно сырые значения и вы сами контролируете экранирование.

Разделы инфоблоков Битрикс: CIBlockSection::GetList и GetByID

Типичный сценарий: каталог уже выводится через CIBlockElement::GetList, но на странице нужен не список товаров, а дерево категорий — в левом меню, в фильтре умного фильтра, в хлебных крошках карточки. Товары лежат в элементах, а сами категории — в отдельной сущности: разделах инфоблока. Их отдаёт класс CIBlockSection, доступный с версии 3.0.4 ядра.

У нас в практике разделы — это четыре постоянных задачи: построить многоуровневое меню каталога, отрисовать хлебные крошки на детальной странице, собрать фильтр «по категориям» и вывести подборку товаров конкретного раздела на главной. Все четыре решаются парой методов — GetList и GetByID.

PHP
<?php
use Bitrix\Main\Loader;
Loader::includeModule('iblock');

$arOrder  = ['SORT' => 'ASC', 'NAME' => 'ASC'];
$arFilter = [
    'IBLOCK_ID' => 5,          // замените на ваш IBLOCK_ID
    'ACTIVE'    => 'Y',
    'GLOBAL_ACTIVE' => 'Y',
    'DEPTH_LEVEL' => 1,        // только верхний уровень
];
$bIncCnt = false;              // не считать элементы в разделе
$arSelect = ['ID', 'NAME', 'CODE', 'SECTION_PAGE_URL', 'IBLOCK_SECTION_ID', 'DEPTH_LEVEL', 'LEFT_MARGIN', 'RIGHT_MARGIN'];
$navStartParams = false;       // без постраничной навигации

$rsSections = CIBlockSection::GetList($arOrder, $arFilter, $bIncCnt, $arSelect, $navStartParams);
while ($arSection = $rsSections->GetNext()) {
    // $arSection['NAME'], $arSection['SECTION_PAGE_URL'] и т.д.
    var_dump($arSection);
}

Ключевое здесь — фильтр и набор полей. Параметр arFilter принимает IBLOCK_ID (обязательно), ACTIVE, GLOBAL_ACTIVE и DEPTH_LEVEL. Именно DEPTH_LEVEL = 1 отделяет корневые разделы от вложенных — без него вы получите плоский список всех категорий сразу, и рисовать из него дерево придётся руками.

Для построения дерева есть два подхода. Первый — рекурсия: берём разделы с IBLOCK_SECTION_ID = 0, для каждого рекурсивно запрашиваем дочерние по IBLOCK_SECTION_ID = $parentId. Второй — одним запросом с полями LEFT_MARGIN и RIGHT_MARGIN (вложенные множества), затем обходом массива по уровням. Первый проще читается и подходит для небольших каталогов. Второй экономнее по запросам, его стоит выбирать, когда разделов сотни и больше.

Что захочется поменять под свой проект: bIncCnt — включите, если рядом с названием категории нужен счётчик товаров (учтите, что это отдельный SQL-запрос на раздел). Поле SECTION_PAGE_URL в Select даёт готовую ссылку с учётом правил ЧПУ — не собирайте URL вручную через CODE. А NavStartParams пригодится, если вы выводите разделы в публичном списке с пагинацией, а не в меню.

Чем GetByID отличается от GetList по правам доступа

Когда нужен один конкретный раздел — например, в хлебных крошках или на странице категории — берут CIBlockSection::GetByID($sectionId). Синтаксис минимален, но есть нюанс, о который спотыкаются: в GetByID проверка прав доступа включена по умолчанию. Метод вернёт раздел только если текущий пользователь имеет право на чтение этого инфоблока.

В CIBlockSection::GetList такой проверки по умолчанию нет — фильтр отработает и вернёт данные независимо от прав. Значит, при вызове из публичной части под гостем GetByID может вернуть false, хотя GetList с тем же ID в фильтре отдаст раздел. В административном интерфейсе или под авторизованным сотрудником разницы вы не заметите. А на фронте получите пустой результат и будете искать ошибку в данных.

Отдельная частая задача — узнать, в каком разделе лежит элемент. Здесь помогает поле IBLOCK_SECTION_ID: запрашиваем его через CIBlockElement::GetByID и получаем ID раздела, который дальше скармливаем в CIBlockSection::GetByID для крошек или заголовка категории.

PHP
<?php
use Bitrix\Main\Loader;
Loader::includeModule('iblock');

$elementId = (int)$_REQUEST['ELEMENT_ID']; // замените на ваш источник ID

$rsElement = CIBlockElement::GetByID($elementId);
if ($arElement = $rsElement->GetNext()) {
    $sectionId = (int)$arElement['IBLOCK_SECTION_ID'];

    if ($sectionId > 0) {
        $rsSection = CIBlockSection::GetByID($sectionId);
        if ($arSection = $rsSection->Fetch()) {
            // $arSection['NAME'] — название раздела для крошек
            // $arSection['SECTION_PAGE_URL'] — ссылка на категорию
        }
    }
}

Дальше эти два метода комбинируются с выводом элементов — об этом в разделе про шаблон компонента.

Свойства инфоблока Битрикс: типы, фильтрация и обновление

Самый частый запрос на эту тему — «почему свойства не выводятся в шаблоне, хотя в админке заполнены». Причина почти всегда одна: свойства инфоблока — это не поля элемента. Поля (NAME, PREVIEW_TEXT, ACTIVE, SORT) лежат в таблице b_iblock_element и доступны в CIBlockElement::GetList напрямую. А свойства хранятся отдельно — в b_iblock_element_property, привязываясь к элементу через IBLOCK_ELEMENT_ID, а к описанию свойства — через IBLOCK_PROPERTY_ID. Отсюда все следствия: чтобы получить значения, ядру нужно сделать дополнительный запрос-джойн, а чтобы обновить — работать не с $arFields, а с отдельным API.

У нас в практике это всплывает в трёх типовых местах. Первое — вывод каталога: разработчик пишет $arItem['PROPERTIES']['AUTHOR']['VALUE'], но в параметрах компонента не указал код свойства, и в $arResult его просто нет. Второе — фильтрация: нужно отобрать товары по обязательным или числовым свойствам, и запрос через arFilter по PROPERTY_* требует понимания, что тип свойства влияет на то, как значение хранится и сравнивается. Третье — обновление: импорт из 1С или собственный скрипт массово перезаписывает свойства, и здесь особенно больно с файловыми — они теряют привязку к файлам, если обновлять поштучно. Разберём все три сценария на реальных вызовах API.

PHP
<?php
// Получаем только обязательные строковые свойства конкретного инфоблока
$rsProperty = CIBlockProperty::GetList(
    ['SORT' => 'ASC', 'NAME' => 'ASC'],
    [
        'IBLOCK_ID'     => 17,          // замените на ваш IBLOCK_ID
        'PROPERTY_TYPE' => 'S',         // S — строка, N — число
        'IS_REQUIRED'   => 'Y',         // только обязательные
    ]
);

while ($arProperty = $rsProperty->Fetch()) {
    echo $arProperty['CODE'] . ' — ' . $arProperty['NAME'] . "\n";
    echo '  тип: ' . $arProperty['PROPERTY_TYPE']
    . ', множественное: ' . $arProperty['MULTIPLE'] . "\n";
}

В этом вызове ключевое — фильтр. PROPERTY_TYPE задаёт тип хранения: S — строка, N — число, F — файл, L — список. От типа зависит и то, как значение ложится в b_iblock_element_property, и как по нему фильтровать элементы: для N сравнение числовое, для S — строковое, для L — по ID значения списка. Признак IS_REQUIRED полезен, когда строите валидацию перед сохранением: получить список обязательных свойств и проверить, что каждое заполнено, дешевле, чем ловить ошибки в момент Update.

Привязка к инфоблоку задаётся через IBLOCK_ID — без него выборка пойдёт по всем инфоблокам и вернёт лишнее. Всегда передавайте IBLOCK_ID в фильтр: это и корректность, и скорость. Если нужны свойства сразу нескольких типов, уберите PROPERTY_TYPE из фильтра и фильтруйте результат в PHP, либо делайте два запроса — по опыту, для каталогов это читаемее, чем один универсальный вызов. Для множественных свойств (MULTIPLE = 'Y') помните, что в b_iblock_element_property на один элемент будет несколько строк — учитывайте это при подсчёте.

Как обновлять файловые свойства через SetPropertyValues

Отдельная головная боль — файловые свойства (PROPERTY_TYPE = 'F'). Значение такого свойства в базе — не сам файл, а ID записи в b_file, и при каждом обновлении ядро создаёт новую запись. Если обновлять значения по одному в цикле, старые ID теряются, а файлы, на которые они ссылались, остаются мусором. Правильный путь — собрать все значения в массив и вызвать CIBlockElement::SetPropertyValues один раз.

PHP
<?php
$elementId = 4521;                      // ID товара
$propertyCode = 'GALLERY';              // код множественного файлового свойства

// Массив абсолютных путей к новым файлам
$filePaths = [
    $_SERVER['DOCUMENT_ROOT'] . '/upload/tmp/photo-1.jpg',
    $_SERVER['DOCUMENT_ROOT'] . '/upload/tmp/photo-2.jpg',
];

// Один вызов — все значения сразу
CIBlockElement::SetPropertyValues(
    $elementId,
    17,                                 // IBLOCK_ID
    $filePaths,                         // массив значений
    $propertyCode
);

Здесь важно, что третий аргумент — именно массив, даже если значение одно: для множественного свойства ядро ждёт список. Для одиночного файлового свойства тоже передавайте массив из одного элемента — иначе поведение зависит от версии и может отличаться. После вызова старые записи в b_iblock_element_property и связанные b_file пересоздаются согласованно, без «висячих» ID.

Грабля: обновление файловых свойств поштучно в цикле — SetPropertyValues с одним значением, потом ещё раз с другим — ломает ID значений. Каждый вызов пересоздаёт записи, ссылки на предыдущие файлы теряются, а сами файлы остаются в b_file мусором. В каталоге это выливается в битые картинки и раздувание таблицы файлов. Обход один: собрать все значения в массив и вызвать SetPropertyValues ровно один раз на свойство. Если нужно обновить несколько разных свойств — делайте отдельный вызов на каждое, но не дробите одно свойство на несколько вызовов.

Как вывести инфоблок Битрикс в шаблоне компонента

Бывает, что штатный компонент уже отдаёт данные в $arResult, но вывод нужно переделать под конкретную вёрстку: слайдер на главной, карточки с нестандартной сеткой, блок «похожие товары» с дополнительными полями. В таких случаях мы не переписываем компонент целиком и не дёргаем CIBlockElement::GetList повторно — берём уже готовый $arResult и оборачиваем его в нужную структуру прямо в файле шаблона template.php. Шаблон компонента — это тот слой, где данные уже собраны, отфильтрованы по правам и закэшированы, а значит, второй запрос к базе не нужен.

Отдельная выгода — переиспользование. Один и тот же компонент может иметь несколько шаблонов: один для десктопа, другой для мобильной выдачи, третий — для промо-лендинга. Вёрстка меняется, а логика выборки остаётся общей. Именно поэтому в проектах с активным 1С-обменом мы стараемся держать всю работу с данными в result_modifier.php, а в template.php — только разметку.

Ниже разберём, как обернуть цикл по элементам в структуру слайдера и как правильно дозапросить свойства, которых в $arResult по умолчанию нет.

PHP
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
?>

<div class="owl-carousel promo-slider">
<?php foreach ($arResult['ITEMS'] as $arItem): ?>
<?php
$this->AddEditAction(
    $arItem['ID'],
    $arItem['EDIT_LINK'],
    CIBlock::GetArrayByID($arItem['IBLOCK_ID'], 'ELEMENT_EDIT')
);
$this->AddDeleteAction(
    $arItem['ID'],
    $arItem['DELETE_LINK'],
    CIBlock::GetArrayByID($arItem['IBLOCK_ID'], 'ELEMENT_DELETE'),
    ['CONFIRM' => GetMessage('CT_BNL_ELEMENT_DELETE_CONFIRM')]
);
?>
<div class="promo-slide" id="<?= $this->GetEditAreaId($arItem['ID']); ?>">
<img src="<?= $arItem['PREVIEW_PICTURE']['SRC']; ?>"
alt="<?= htmlspecialchars($arItem['NAME']); ?>">
<h3><?= $arItem['NAME']; ?></h3>
<p><?= $arItem['PROPERTIES']['AUTHOR']['VALUE']; ?></p>
<time><?= $arItem['PROPERTIES']['DATE_PUBLIC']['VALUE']; ?></time>
</div>
<?php endforeach; ?>
</div>

Ключевое здесь — коды свойств должны быть перечислены в параметрах вызова компонента. Если в вызове не указать 'PROPERTY_CODE' => ['AUTHOR', 'DATE_PUBLIC'], то в $arResult['ITEMS'] массив PROPERTIES будет пустым, и обращение к $arItem['PROPERTIES']['AUTHOR']['VALUE'] вернёт null. Это самая частая причина «пустых полей в шаблоне». Компонент не тянет все свойства подряд — он собирает только те, что явно запрошены, чтобы не плодить лишние JOIN'ы к таблицам значений.

Что стоит менять под свой проект: имя CSS-класса слайдера (у нас owl-carousel, но у вас может быть swiper или собственный контейнер), набор полей в карточке, наличие AddEditAction — в шаблонах для публичной части без режима правки его можно убрать. Инициализацию скрипта слайдера выносим в script.js шаблона, а не в инлайн — так подключается штатный механизм отложенных скриптов Битрикса.

Что делать в result_modifier.php

Если в шаблоне нужны данные, которых нет в $arResult — например, раздел элемента или свойство, не входящее в PROPERTY_CODE, — дозапрашиваем их в result_modifier.php. Этот файл подключается ядром до вывода шаблона и работает с тем же $arResult, что и template.php. Логика простая: собираем ID элементов, одним запросом достаём нужное, раскладываем обратно в $arResult['ITEMS']. Так мы не плодим N+1 запросов на каждой карточке.

PHP
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

$elementIds = array_column($arResult['ITEMS'], 'ID');
if (empty($elementIds)) {
    return;
}

$sectionsMap = [];
$rsSections = CIBlockElement::GetElementGroups($elementIds, true, ['ID', 'NAME', 'CODE']);
while ($arSection = $rsSections->Fetch()) {
    $sectionsMap[$arSection['ID']] = $arSection;
}

$rsElements = CIBlockElement::GetList(
    [],
    ['ID' => $elementIds],
    false,
    false,
    ['ID', 'IBLOCK_SECTION_ID']
);
while ($arElement = $rsElements->Fetch()) {
    $sectionId = (int)$arElement['IBLOCK_SECTION_ID'];
    if (isset($sectionsMap[$sectionId])) {
        $arResult['SECTIONS_MAP'][$arElement['ID']] = $sectionsMap[$sectionId];
    }
}

Дальше в template.php к нужной карточке подтягиваем название раздела через $arResult['SECTIONS_MAP'][$arItem['ID']]['NAME']. Один запрос на всю страницу вместо запроса в цикле. По факту это главное, ради чего вообще трогают result_modifier.php.

Форма добавления и редактирования элемента: почему не подхватывается ID

Отдельная головная боль — фронтенд-редактирование пользовательского контента. Объявления на доске, отзывы с фотографиями, личные карточки сотрудников в корпоративном портале: пользователь должен добавить запись, а потом вернуться и поправить её через ту же форму. Штатный bitrix:iblock.element.add.form умеет оба режима — добавление и редактирование — и в теории переключается автоматически, если в запросе есть идентификатор элемента. На практике разработчики хватаются за голову: форма добавления работает, а при переходе по ссылке вида ?ID=123 поля остаются пустыми, будто элемента не существует. Приходится либо городить костыль с перезаписью $_GET, либо отказываться от штатного компонента в пользу самописной формы на CIBlockElement::SetPropertyValues.

Мы в таких случаях не спешим выкидывать компонент — причина обычно в одном неочевидном месте, и лечится она одной строкой. Разберём, что именно ломается и как настроить «красивые» адреса редактирования, чтобы не возвращаться к этому вопросу.

Компонент bitrix:iblock.element.add.form определяет режим работы по переменной $_REQUEST['CODE'] — это имя ключа в запросе, а не символьный код элемента. Если в $_REQUEST['CODE'] лежит число, компонент считает его ID элемента и подтягивает поля для редактирования. Если ключа нет — работает как форма добавления.

Классическая ошибка — передать идентификатор через ?ID=123. Компонент такое просто не видит: он смотрит на CODE, и в $_REQUEST этого ключа не оказывается. Поля пустые не потому, что элемент не найден, а потому, что компонент даже не пытался его искать.

Что тут важно для адаптации под свой проект:

  • Имя ключа CODE — не догма, но переопределяется оно только правкой шаблона компонента или обёрткой; в 99% проектов проще подстроиться под него.
  • Если у вас в URL уже есть параметр CODE с символьным кодом элемента — конфликт. Придётся либо переименовать его, либо передавать ID отдельным ключом ELEMENT_ID и доопределять $_REQUEST['CODE'] в result_modifier.php.
  • Для «красивых» адресов вида /board/edit/123/ ID достаётся из URL-правила — это уже задача urlrewrite.php, а не самого компонента.

Передаём ID в нужный ключ и настраиваем правило разбора адреса:

PHP
// result_modifier.php шаблона компонента — доопределяем CODE из ЧПУ
if (preg_match('#^/board/edit/(\d+)/#', $_SERVER['REQUEST_URI'], $m)) {
    $_REQUEST['CODE'] = (int)$m[1];
}

// urlrewrite.php — правило для /board/edit/123/
$arUrlRewrite = [
    [
        'CONDITION' => '#^/board/edit/([0-9]+)/#',
        'RULE'      => 'CODE=$1',
        'ID'        => '',
        'PATH'      => '/board/edit.php',
    ],
];

Грабля со слешем в конце. Если в CONDITION правила оставить завершающий слеш — например #^/board/edit/([0-9]+)/# вместо #^/board/edit/([0-9]+)# — Битрикс не подхватит ваше правило и уйдёт в стандартные правила комплексного компонента. Вместо обработки формы он начнёт искать элемент инфоблока по этому адресу и отдаст 404 или пустую страницу. Убирайте слеш в конце условия — это ровно тот случай, который разбирали на ru.stackoverflow (вопрос про правила ЧПУ для пагинации): симптом тот же, причина та же.

ЧПУ для пагинации в разделе инфоблока: где ломается правило

Распространённая ошибка: правило ЧПУ для постраничной навигации добавлено в urlrewrite.php, но вторая страница раздела всё равно открывается как /actions/?PAGEN_1=2. С такой жалобой приходят чаще всего после того, как настроили «красивые» адреса для детальных страниц и разделов — и решили, что с пагинацией всё заработает «по аналогии».

На ru.stackoverflow разбирали ровно этот случай: в разделе акций комплексный компонент bitrix:news не отдавал /actions/page2, подставляя PAGEN_1 в query-строку. Причина оказалась не в самом правиле, а в одном символе внутри CONDITION.

Ключевой момент — слеш в конце регулярного выражения внутри CONDITION. Когда условие выглядит как #^/actions/page([0-9]+)/#, Битрикс при разборе URL начинает примерять его в цепочке правил комплексного компонента. Слэш на конце заставляет движок считать, что адрес /actions/page2/ — это путь к элементу с символьным кодом page2 внутри раздела /actions/. Дальше срабатывает стандартное правило детальной страницы, компонент пытается найти элемент с таким кодом, не находит — и откатывается на навигацию через GET-параметр. Визуально это выглядит так, будто ваше правило вообще не подключилось.

Убираем слеш — и /actions/page2 перестаёт совпадать с шаблоном детальной страницы. Правило наконец перехватывает URL и раскладывает его в PAGEN_1=2 относительно /actions/.

Если в проекте разделы живут с завершающим слешем (например, каталог настроен на /catalog/), проверьте оба варианта условия и оставьте тот, что реально приходит в $_SERVER['REQUEST_URI'] после редиректов. Второй момент — шаблон пагинации: даже с рабочим правилом компонент по умолчанию строит ссылки через GET, поэтому в параметрах компонента нужно переключить PAGER_TEMPLATE на шаблон, который формирует ЧПУ-адреса вида /actions/page2/.

PHP
<?php
// urlrewrite.php — правило для пагинации в разделе /actions/
// Обратите внимание: в CONDITION нет завершающего слеша.
$arUrlRewrite = [
    [
        "CONDITION" => "#^/actions/page([0-9]+)#",
        "RULE"      => 'PAGEN_1=$1',
        "PATH"      => "/actions/",
    ],
];

// В вызове комплексного компонента bitrix:news
// переключаем шаблон постраничной навигации на ЧПУ-вариант:
$APPLICATION->IncludeComponent(
    "bitrix:news",
    "actions",
    [
        "IBLOCK_ID"     => 12, // замените на ваш IBLOCK_ID
        "SEF_MODE"      => "Y",
        "PAGER_TEMPLATE" => "arrows_chpu", // шаблон с ЧПУ-ссылками
        // ... остальные параметры
    ]
);

D7 ORM против классического API: что выбрать для инфоблоков

На форуме разработчиков регулярно всплывает один и тот же спор. Одни коллеги утверждают: инфоблоки «нельзя считать через D7» — потому что свойства разложены по отдельным таблицам (b_iblock_element_property, b_iblock_property), связи через ELEMENT_PROPERTY тянутся дополнительными джойнами, и ORM тут только мешает. Другие настаивают: D7 — современный стандарт, а CIBlockElement::GetList — это легаси, от которого надо уходить. Обе крайности — от непонимания, где какой инструмент уместен.

У нас в практике выбор диктует задача, а не идеология. Если нужно быстро вывести 20 товаров в слайдер главной — классический API короче и понятнее. Если пишем собственный модуль с бизнес-логикой, интеграцию с внешней системой или фоновую обработку — D7 ORM даёт типизацию, события и нормальную работу с сущностями. Разберём, где что выигрывает.

Критерий CIBlockElement::GetList D7 getList
Свойства Подтягиваются автоджойном Требуют явного указания в select
Кеш Управляется компонентом Настраивается вручную через Cache
Читаемость Массивы, «магия» ключей Объекты, автокомплит в IDE
Поддержка Стабильно, но не развивается Актуальный стандарт ядра

Наша рабочая позиция — не смешивать оба API в одном модуле. Разделяем по слоям. В шаблонах компонентов и в публичке остаёмся на классике: CIBlockElement::GetList уже умеет подтягивать свойства, а компоненты сами управляют кешем — переписывать это на D7 смысла нет.

А вот в собственных модулях, агентах, обработчиках 1С-обмена и CLI-скриптах берём ORM: там нужны транзакции, события через EventManager и предсказуемая типизация. Ключевое условие для D7 — у инфоблока должен быть задан «Символьный код API» в настройках (строка от 1 до 50 символов). Без него ядро не сгенерирует ORM-класс для сущности, и getList() работать не будет. Это первое, что мы проверяем, когда к нам приходят с вопросом «почему ORM не видит инфоблок».

Аналог CIBlockElement::GetById() в D7 — getByPrimary(), аналог GetList() - getList(). Синтаксис другой, но логика та же. Выбор варианта простой: если в задаче есть бизнес-логика и её будут поддерживать — D7; если это разовый вывод для вёрстки — классика.

Самые частые ошибки при работе с инфоблоками

За годы работы с чужим кодом у нас сложился устойчивый топ грабель по инфоблокам. Почти каждая проявляется не сразу, а спустя месяцы, когда каталог разрастается. Самая коварная — отсутствие IBLOCK_ID в фильтре запроса. Пока инфоблок в проекте один, всё работает. Как только появляется второй — под баннеры, под акции, под сотрудников — выборка начинает тянуть элементы из чужих инфоблоков: в каталоге всплывают баннеры, в слайдере новостей — товары. Отловить это сложно, потому что код «не падает», просто выдаёт лишнее.

Второй по частоте источник проблем — работа с методами выборки: разработчики берут GetNext() там, где нужен Fetch(), и получают HTML-экранирование полей и префиксы у свойств. Третья грабля — правила ЧПУ и обновление файловых свойств, где API ведёт себя неочевидно. Разберём каждую.

Грабля Причина Как исправить
Нет IBLOCK_ID в фильтре Выборка тянет элементы из чужих инфоблоков, падает скорость Всегда указывать IBLOCK_ID в arFilter
GetNext() вместо Fetch() Поля HTML-экранируются, к свойствам клеятся префиксы Использовать Fetch() в цикле выборки
Слеш в конце urlrewrite Битрикс уходит на стандартные правила и ищет элемент вместо пагинации Убрать слеш в конце CONDITION
Поштучное обновление файловых свойств ID значений меняются при каждом вызове Собрать значения в массив и обновить одним вызовом

Перед выкаткой мы прогоняем короткий чеклист — он ловит большинство этих грабель ещё до продакшена. Если вывод инфоблока сломался, смотреть в первую очередь стоит на фильтр запроса: есть ли там IBLOCK_ID, не потерялся ли он при рефакторинге. Дальше — на метод выборки в цикле: Fetch() или GetNext().

Если «поехали» свойства — проверить, что в arSelectFields переданы ID и IBLOCK_ID, иначе CIBlockElement::GetProperties молча вернёт пустоту. Если ломается пагинация — открыть urlrewrite.php и посмотреть на CONDITION: лишний слеш в конце регулярки уводит обработку на стандартные правила компонента. И последнее — файловые свойства: обновлять их только пакетно, одним вызовом SetPropertyValues, потому что ID значений меняются на каждом шаге. Такой чеклист из пяти пунктов закрывает почти все инциденты, которые к нам приносят на разбор.

Читайте также: Не работает поиск на Битрикс: фасетный индекс, переиндексация и модуль search.

Подробнее об услуге: разработка корпоративных сайтов на Битрикс.

Частые вопросы

Можно ли получить значения свойств элементов одним запросом, не вызывая CIBlockElement::GetProperty в цикле?

Да, укажите в $arSelect символы свойств (например, "PROPERTY_PRICE") или передайте в $arParams['SELECT'] — значения придут в массиве PROPERTIES. Для множественных свойств используйте ключ вида PROPERTY_ARTICLE с суффиксом _VALUE.

Что делать, если CIBlockElement::GetList возвращает пустой результат, хотя элементы в админке есть?

Проверьте три вещи: активность элемента (ACTIVE=Y), активность инфоблока и привязку к разделам через IBLOCK_SECTION_ID или SECTION_ID в фильтре. Также убедитесь, что не задан фильтр по свойству с неверным CODE — фильтр по свойству требует ключа вида PROPERTY_ARTICLE.

Чем отличается CIBlockSection::GetList от CIBlockSection::GetByID при выборке раздела?

GetByID возвращает один раздел по ID без фильтрации по активности и правам, а GetList принимает массив фильтров и параметров (SELECT, ORDER, COUNT) и учитывает ACTIVE. Для публичной части используйте GetList с фильтром ACTIVE=Y.

А если у меня свойство типа «Список» и нужно отфильтровать элементы по нескольким значениям сразу?

В фильтре передайте массив значений: "PROPERTY_COLOR" => array("red", "blue"). Битрикс сгенерирует условие IN по таблице свойств, но учтите, что для множественных свойств это работает через отдельный JOIN и может замедлить запрос.

Можно ли обновить значение свойства элемента через CIBlockElement::SetPropertyValuesEx, не затрагивая остальные свойства?

Да, SetPropertyValuesEx обновляет только переданные свойства, остальные остаются без изменений. Чтобы очистить свойство, передайте пустое значение или false, а для множественного — массив.

Что делать, если после включения ЧПУ пагинация в разделе инфоблока отдаёт 404?

Проверьте, что в urlrewrite.php или .htaccess правило обрабатывает параметр PAGEN_1, а в настройках компонента включён режим ЧПУ с шаблоном вида /catalog/#SECTION_CODE#/. Часто проблема в том, что шаблон пути не содержит #SECTION_CODE_PATH# или не совпадает с реальной структурой разделов.

#Bitrix #PHP #MySQL #SEO
автор · Backend / SRE Engineer
Артем Колячек

Backend-разработчик и SRE в студии Paradigma. Занимается тем, что у других проектов обычно обнаруживается за неделю до запуска: переездом Bitrix-проектов между серверами, починкой кешей после миграций, выстраиванием pipeline для контейнеров, мониторингом под нагрузкой.

До Paradigma — 7 лет в backend (PHP + Postgres) и в operations (Linux, Docker, Coolify, restic-бэкапы). Любит когда логи разговаривают полным синтаксисом ошибки, а не «что-то пошло не так».

На блоге пишет ровно про те ситуации с которыми сам разбирался руками: какой Bitrix-апдейт сломал кеш и как откатить, почему Docker-сеть не находит контейнер после рекрейта, что делать когда asyncpg ругается на event loop.