Инфоблоки Битрикс: что это и из чего состоит
Когда к нам приходят с проектом, где каталог перевалил за 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
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
$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
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
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
// Получаем только обязательные строковые свойства конкретного инфоблока
$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
$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
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
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 в нужный ключ и настраиваем правило разбора адреса:
// 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
// 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# или не совпадает с реальной структурой разделов.