Разработчик открывает старый проект на 1С-Битрикс и видит знакомую картину: CIBlockElement::GetList с массивами фильтров, $arSelect со строковыми ключами, CModule::IncludeModule в начале каждого файла и глобальные объекты вроде $USER и $APPLICATION. Всё работает, но код нечитаемый, ошибки в фильтрах всплывают только в рантайме, а IDE не подсказывает ни методов, ни полей. Рядом в той же системе живёт другой мир — ядро D7: пространства имён Bitrix\Main, автозагрузка классов, ORM с DataManager и типизированными выборками. Вопрос не в том, «работает ли старое API» (работает), а в том, как писать новое, чтобы проект было приятно поддерживать и расширять.
Эта статья — практическая карта D7 и ORM для тех, кто пишет на Битриксе и хочет уйти от «магических массивов» старого ядра к современному коду. Разберём, что такое ядро D7 в отличие от старого API, как устроены пространства имён и автозагрузка, что делают Application, Context и сервис-локатор, как работает ORM — Entity, DataManager, методы add/update/delete/getList, поля ScalarField и Reference, связи и join, рантайм-поля и агрегаты, кэш выборок и транзакции, а также HighloadBlock через ORM. Покажем, чем D7 объективно лучше CIBlockElement::GetList и CModule, как совмещать старое и новое и как аккуратно мигрировать. Материал — выжимка из наших highload-проектов на 1С-Битрикс, где цена читаемости и скорости кода особенно высока.
Коротко
- D7 — современное объектное ядро Битрикса: пространства имён (
Bitrix\Main,Bitrix\Iblock), автозагрузка классов и ORM вместо процедурного старого API. - ORM строится на
DataManager: класс описывает таблицу и поля, аadd/update/delete/getListдают типизированную и безопасную работу с данными. - getList d7 принимает понятную структуру
select/filter/order/runtime; связи черезReferenceиjoin, агрегаты — через рантайм-поля. - D7 лучше
CIBlockElement::GetListпо типизации, читаемости и безопасности; но старое и новое ядро сосуществуют — их можно совмещать. - HighloadBlock изначально на ORM: своя таблица плюс
DataManager— идеально для больших справочников. Миграцию делают модуль за модулем, без «переписать всё сразу».
Зачем разработчику переходить на D7
1С-Битрикс тащит на себе тяжёлое наследие: платформа развивается с начала 2000-х, и старое процедурное API — часть её ДНК. Оно никуда не денется, потому что на нём написаны миллионы строк рабочего кода. Но писать новую логику в старом стиле в 2020-х — значит сознательно усложнять себе жизнь. D7 существует именно для того, чтобы новый код был чище, безопаснее и быстрее в разработке.
Переход на D7 даёт три практических выигрыша, которые чувствуются на каждом проекте:
- Читаемость. Типизированные классы и понятная структура выборок вместо массивов со строковыми ключами. Код читается как код, а не как ребус из префиксов
>=,PROPERTY_*и~. - Безопасность. ORM параметризует запросы автоматически — SQL-инъекция «из коробки» не пройдёт там, где в старом API легко забыть экранирование. Плюс IDE подсказывает методы и не даёт опечататься в имени поля.
- Скорость разработки. Автозагрузка убирает ручные
require, связи черезReferenceзаменяют вложенные циклы выборок, а рантайм-поля собирают сложные отчёты одним запросом.
Обратная сторона — стоимость незнания. Проекты, где всё написано «по-старому», со временем становится всё труднее развивать: логика размазана по глобальным функциям, тесты писать невозможно, а любая правда о данных достаётся только через отладочный print_r. D7 не волшебная таблетка, но это тот фундамент, на котором современный код Битрикса держится десятилетие. Разберёмся, что это за фундамент.
Что такое ядро D7 и как оно появилось
«D7» — это сокращение от «ядро версии D7» (по внутренней нумерации платформы). Исторически у Битрикса было старое ядро — набор глобальных классов с префиксом C (CIBlockElement, CUser, CModule, CSaleOrder) и функций, живущих в глобальном пространстве. Оно процедурно-объектное: классы есть, но без пространств имён, без автозагрузки, с массивами вместо типизированных структур. Это работало годами, но плохо масштабировалось под современные требования к архитектуре.
D7 — это переписанное на нормальном ООП ядро, которое ввели, чтобы платформа говорила на языке современного PHP: пространства имён, автозагрузка по стандарту, статические фабрики, ORM, событийная модель нового образца. Ключевая мысль: D7 не отдельный продукт и не «новый Битрикс» — это второй, современный слой того же самого ядра. Он живёт в тех же модулях, что и старое API, просто в другом пространстве имён.
Практически это выглядит так. Функциональность модуля main в старом стиле — это CUser, CFile, CSite; в D7 — пространство имён Bitrix\Main с классами Bitrix\Main\UserTable, Bitrix\Main\Application, Bitrix\Main\Type\DateTime. Функциональность модуля инфоблоков в старом стиле — CIBlockElement, CIBlockSection; в D7 — Bitrix\Iblock\ElementTable, Bitrix\Iblock\SectionTable и объектный API инфоблоков. Оба набора доступны в любой точке кода после подключения модуля.
C-классами или пространствами имён Bitrix\*.
Старое ядро и D7: два слоя одной платформы
Чтобы уверенно писать на Битриксе, нужно держать в голове картину двух слоёв. Снизу — общая инфраструктура: база данных, файловая система, модули. Над ней — два параллельных API к одной и той же функциональности: старое процедурно-объектное (C-классы и функции) и современное D7 (пространства имён и ORM). Они не конкурируют за данные — работают с одними и теми же таблицами, просто по-разному их выражают.
Это объясняет, почему миграция на D7 не требует «всё сломать и переписать». Вы можете в одном контроллере выбрать заказы через Bitrix\Sale\Order и тут же обратиться к старому CSaleBasket, если так проще. Данные согласованы, потому что слой хранения общий. Разница только в удобстве и безопасности того кода, который пишете вы.
Ниже — принципиальная схема соотношения слоёв. Она хорошо укладывает в голову главную мысль: D7 не «поверх» и не «вместо» старого ядра, а рядом, как второй фасад к общей инфраструктуре.
Пространства имён и автозагрузка классов
Первое, что отличает код D7 внешне, — обращения к классам вида \Bitrix\Main\Type\DateTime вместо CIBlockElement. Это пространства имён (namespaces). Они решают две задачи: устраняют конфликты имён (в модулях main и sale могут быть классы с одинаковым коротким именем) и делают путь к исходнику предсказуемым — по полному имени класса сразу понятно, из какого модуля он и где лежит файл.
Вторая половина картины — автозагрузка. В старом коде классы подключались вручную: чтобы использовать функциональность модуля, писали CModule::IncludeModule('iblock'), а иногда и прямые require. В D7 классы загружаются сами: когда PHP впервые встречает обращение к \Bitrix\Iblock\ElementTable, автозагрузчик Битрикса по namespace вычисляет путь к файлу и подключает его. Никаких ручных require для ядра.
На практике типичный «пролог» файла D7 выглядит так — подключаем модуль (для регистрации его автозагрузки) и объявляем алиасы use для коротких имён:
<?php
use Bitrix\Main\Loader;
use Bitrix\Iblock\ElementTable;
use Bitrix\Main\Type\DateTime;
// Регистрируем модуль (и его автозагрузку классов) — современный аналог CModule
if (!Loader::includeModule('iblock')) {
throw new \RuntimeException('Модуль iblock не установлен');
}
// Дальше классы модуля доступны без единого require:
$now = new DateTime(); // \Bitrix\Main\Type\DateTime
$rows = ElementTable::getList([...]); // \Bitrix\Iblock\ElementTable
Обратите внимание на Loader::includeModule() — это D7-замена CModule::IncludeModule(). Она возвращает bool, и хорошая привычка — проверять результат, а не вызывать «на удачу». Для своих классов автозагрузку регистрируют через описание в собственном модуле или через Loader::registerAutoLoadClasses(), указывая соответствие имени класса и файла. Тогда ваш \Vendor\Shop\PriceCalculator будет подгружаться так же автоматически, как классы ядра.
Ниже — как namespace превращается в путь к файлу. Именно эта однозначность и делает автозагрузку возможной.
Application, Context и сервис-локатор
В старом коде «точки входа» в систему — глобальные объекты: $APPLICATION, $USER, $DB. Они удобны, но это глобальное состояние, которое трудно тестировать и легко случайно сломать. В D7 те же сущности доступны через объект приложения — \Bitrix\Main\Application, — который выступает единой точкой доступа к сервисам системы.
Ключевые вещи, которые дают Application и связанный с ним Context:
- Соединение с базой —
Application::getConnection()вместо глобального$DB. Через него открывают транзакции и, при необходимости, выполняют прямой SQL. - Контекст запроса —
Application::getInstance()->getContext(): доступ к параметрам запроса (getRequest()), серверу, текущему сайту, языку. Это типизированная замена работе напрямую с$_GET/$_POST/$_SERVER. - Пул соединений и менеджеры — база, кэш, управляемый кэш доступны как сервисы, а не как разрозненные глобальные переменные.
<?php
use Bitrix\Main\Application;
// Соединение с базой из пула — для транзакций и низкоуровневых операций
$connection = Application::getConnection();
// Контекст запроса вместо прямого доступа к суперглобальным массивам
$request = Application::getInstance()->getContext()->getRequest();
$page = (int)($request->get('PAGEN_1') ?? 1); // безопаснее, чем $_GET['PAGEN_1']
$isPost = $request->isPost();
В новых версиях ядра к этому добавился сервис-локатор — \Bitrix\Main\DI\ServiceLocator. Идея та же, что в любом контейнере зависимостей: сервисы (объекты, реализующие какую-то задачу) регистрируются под именем, а код получает их по этому имени, не создавая вручную. Это шаг к тому, чтобы бизнес-логику можно было подменять и тестировать, а не «прибивать гвоздями» к глобальным объектам.
Application::getConnection() или ServiceLocator::getInstance()->get(...), знайте — это осознанный уход от глобального состояния. Такой код проще переносить, тестировать и читать, потому что зависимости получаются явно, а не берутся «из воздуха» из глобальных переменных.
ORM: Entity, DataManager и таблица
Сердце D7 для повседневной работы — ORM (Object-Relational Mapping), объектное отображение данных. Его задача — связать таблицу в базе с классом в коде так, чтобы вы работали не с сырым SQL и не с безымянными массивами, а с описанной сущностью. Три понятия, которые нужно уложить в голову: таблица, Entity (сущность) и DataManager.
- Таблица — физическая таблица в базе данных (например,
b_iblock_elementили вашаmy_shop_orders_log). - DataManager — базовый класс ORM. Вы наследуете от него свой класс, в котором описываете имя таблицы и её поля. Этот класс — «менеджер данных» таблицы: через него читают и пишут.
- Entity — объект сущности, который ORM строит по описанию класса. Он знает поля, связи, типы; через него доступны служебные операции. В коде вы обычно работаете с классом-наследником
DataManager, аEntityиспользуется «под капотом» и в продвинутых сценариях.
Минимальный класс-наследник DataManager выглядит так — он описывает таблицу и её поля декларативно:
<?php
namespace Vendor\Shop;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\DatetimeField;
class OrdersLogTable extends DataManager
{
// Какую таблицу обслуживает этот класс
public static function getTableName()
{
return 'my_shop_orders_log';
}
// Описание полей таблицы — типизированно и явно
public static function getMap()
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('ORDER_ID', ['required' => true]),
new StringField('STATUS', ['required' => true]),
new DatetimeField('CREATED_AT'),
];
}
}
Всё — этого достаточно, чтобы получить полноценный API к таблице: OrdersLogTable::add(), ::update(), ::delete(), ::getList(). Ни строчки SQL, ни ручного экранирования. Метод getMap() — это контракт: он описывает поля, их типы и ограничения (primary, required, значения по умолчанию). ORM по нему знает, как строить запросы и как валидировать данные.
DataManager: add, update, delete
Запись данных через ORM — это три метода класса-наследника: add(), update(), delete(). Все они возвращают объект результата (Result), у которого можно спросить, успешна ли операция и какие были ошибки. Это принципиально удобнее старого стиля, где результат — то false, то ID, то предупреждение в глобальном объекте.
Добавление. Передаём массив полей, получаем результат с ID новой записи:
<?php
use Vendor\Shop\OrdersLogTable;
use Bitrix\Main\Type\DateTime;
$result = OrdersLogTable::add([
'ORDER_ID' => 5540,
'STATUS' => 'new',
'CREATED_AT' => new DateTime(),
]);
if ($result->isSuccess()) {
$newId = $result->getId();
} else {
// Ошибки валидации доступны явно — не нужно ловить их из глобалей
$errors = $result->getErrorMessages();
}
Обновление. Первым аргументом — первичный ключ, вторым — изменяемые поля:
<?php
$result = OrdersLogTable::update($newId, [
'STATUS' => 'paid',
]);
if (!$result->isSuccess()) {
// обработка ошибок
}
Удаление. Достаточно первичного ключа:
<?php
$result = OrdersLogTable::delete($newId);
Обратите внимание на единый паттерн: любая операция возвращает Result, вы проверяете isSuccess() и при необходимости читаете getErrorMessages(). Это делает обработку ошибок явной и одинаковой во всём проекте. Для массовых операций есть свои приёмы (например, updateByFilter и deleteByFilter в поддерживающих их сущностях), но базовый контракт остаётся тем же.
DataManager для своих таблиц пишет прямо в них, без бизнес-логики модуля. Для сущностей ядра (элементы инфоблоков, заказы) писать через «сырой» табличный класс опасно — вы обойдёте события, права и пересчёты. Для таких данных используйте специализированный объектный API (например, объекты заказа в Bitrix\Sale) или, где его ещё нет, старое API. Про это подробнее в разделе о совместимости.
getList в D7: чтение данных
Главный метод чтения в ORM — getList(). Именно он заменяет CIBlockElement::GetList и делает выборки читаемыми. Он принимает один ассоциативный массив параметров с понятными ключами:
| Ключ | Назначение |
|---|---|
select | Какие поля выбрать (в том числе через связи и рантаймы) |
filter | Условия отбора; операторы задаются в ключе (>=, @, %, !) |
order | Сортировка: поле => ASC/DESC |
limit / offset | Ограничение количества и смещение (пагинация) |
group | Группировка для агрегатов |
runtime | Временные поля: связи, выражения, агрегаты «на лету» |
cache | Кэширование выборки (время жизни, кэш join-ов) |
Базовая выборка выглядит так — сравните читаемость со старым API:
<?php
use Vendor\Shop\OrdersLogTable;
$rows = OrdersLogTable::getList([
'select' => ['ID', 'ORDER_ID', 'STATUS', 'CREATED_AT'],
'filter' => [
'=STATUS' => 'paid', // точное равенство
'>=CREATED_AT' => '2025-01-01 00:00:00',
],
'order' => ['CREATED_AT' => 'DESC'],
'limit' => 50,
])->fetchAll();
foreach ($rows as $row) {
// $row — обычный ассоциативный массив выбранных полей
echo $row['ORDER_ID'] . ' — ' . $row['STATUS'];
}
Несколько важных деталей. Метод возвращает объект-курсор; fetchAll() отдаёт все строки массивом, а fetch() — по одной в цикле (экономнее по памяти на больших выборках). Операторы фильтра задаются префиксом в ключе: = — равно, ! — не равно, >=/<= — сравнение, % — LIKE, @ — IN (значение-массив). Всё это параметризуется автоматически — SQL-инъекции здесь неоткуда взяться, если вы не собираете сырые выражения руками.
Для инфоблоков есть готовые сущности ядра — \Bitrix\Iblock\ElementTable. Через них товары и элементы выбираются тем же getList, и это прямая типизированная замена CIBlockElement::GetList. Как это связано с общей вёрсткой каталога и компонентами, мы разбираем в статье про шаблоны и компоненты 1С-Битрикс — там видно, где заканчивается выборка данных и начинается их вывод.
Поля сущности: ScalarField и типы
Описание полей в getMap() — не формальность, а то, что делает ORM типизированной. Все поля наследуют от базового ScalarField (скалярное поле — то, что хранится в столбце) и уточняют тип: целое, строка, дата, булево, число с плавающей точкой, текст. Тип определяет, как значение приводится при чтении и записи и как строится условие в фильтре.
| Класс поля | Тип данных | Пример столбца |
|---|---|---|
IntegerField | Целое число | ID, ORDER_ID, количество |
StringField | Строка | STATUS, код, артикул |
TextField | Длинный текст | Комментарий, описание |
BooleanField | Логическое (Y/N, 1/0) | ACTIVE, IS_PAID |
FloatField | Число с точкой | Цена, вес, коэффициент |
DatetimeField / DateField | Дата и время / дата | CREATED_AT, DATE |
EnumField | Перечисление допустимых значений | Ограниченный статус |
ReferenceField | Связь с другой сущностью | Связь заказа с пользователем |
ExpressionField | Вычисляемое выражение / агрегат | SUM, COUNT, CONCAT |
У каждого поля есть параметры: primary (первичный ключ), autocomplete (автоинкремент), required (обязательное), default_value (значение по умолчанию), values (для булевых — пара значений Y/N). Есть и валидаторы — например, ограничение длины строки — которые проверяют данные до записи и возвращают ошибку в Result, если значение не проходит. Это ещё один уровень защиты: некорректные данные не долетают до базы.
<?php
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\Validators\LengthValidator;
new StringField('STATUS', [
'required' => true,
'validation' => function () {
return [ new LengthValidator(null, 32) ]; // не длиннее 32 символов
},
]);
Такое явное описание — то, чего принципиально не хватает старому API: там структура данных нигде не объявлена, и о полях узнаёшь только из документации или отладки. В D7 getMap() — это самодокументируемый контракт таблицы.
Связи и Reference: join между таблицами
Настоящая сила ORM раскрывается на связях. В старом API, чтобы получить к товарам данные из другой таблицы, приходилось делать вложенные выборки в цикле (проблема N+1 запросов) или писать сырой SQL с JOIN вручную. В D7 связь описывается один раз через ReferenceField, и дальше данные связанной сущности «приклеиваются» к выборке одним getList.
Связь объявляют в getMap(): указывают имя связи, класс связанной сущности и условие соединения (ON) через Join::on():
<?php
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\UserTable;
// В getMap() сущности заказов — связь с пользователем по USER_ID
new Reference(
'USER', // имя связи
UserTable::class, // связанная сущность
Join::on('this.USER_ID', 'ref.ID') // условие соединения
);
После этого в select и filter можно обращаться к полям связанной сущности через точку — ORM сама построит нужный JOIN:
<?php
$rows = OrdersTable::getList([
'select' => [
'ID',
'PRICE',
'USER_NAME' => 'USER.NAME', // поле из связанной сущности
'USER_EMAIL' => 'USER.EMAIL',
],
'filter' => [
'=USER.ACTIVE' => 'Y', // фильтр по связанной таблице
'>PRICE' => 0,
],
'order' => ['ID' => 'DESC'],
])->fetchAll();
Одним запросом мы получили заказы вместе с именем и e-mail пользователя, отфильтровав по активности пользователя. Никаких вложенных циклов, никакого ручного SQL, и проблема N+1 решена структурно. Тип связи по умолчанию — LEFT JOIN (при выборе поля связи), но при использовании связи в фильтре ORM применяет INNER JOIN; поведением можно управлять параметром join_type.
Runtime-поля и агрегаты
Не каждую связь или расчёт имеет смысл «зашивать» в класс сущности навсегда. Для разовых, отчётных или контекстных нужд в D7 есть runtime-поля — виртуальные поля, которые вы добавляете прямо в вызов getList через параметр runtime. Они существуют только в рамках этой выборки.
Runtime-поля бывают трёх видов, и все три невероятно полезны:
- Временная связь (
Referenceв runtime) — приклеить другую таблицу, не объявляя связь вgetMap(). - Вычисляемое выражение (
ExpressionField) — посчитать что-то по SQL-функции: конкатенация, приведение, арифметика. - Агрегат — частный случай выражения:
SUM,COUNT,AVG,MIN,MAXв связке сgroup.
Классический пример — посчитать количество и сумму заказов по каждому пользователю. Это агрегат с группировкой, и в ORM он собирается декларативно:
<?php
use Bitrix\Main\ORM\Fields\ExpressionField;
$stats = OrdersTable::getList([
'select' => [
'USER_ID',
'CNT' => new ExpressionField('CNT', 'COUNT(*)'),
'TOTAL' => new ExpressionField('TOTAL', 'SUM(%s)', ['PRICE']),
],
'filter' => ['=STATUS' => 'paid'],
'group' => ['USER_ID'],
'order' => ['TOTAL' => 'DESC'],
])->fetchAll();
// Каждая строка: USER_ID, CNT (число заказов), TOTAL (сумма)
Обратите внимание на плейсхолдер %s в ExpressionField — он безопасно подставляет имя поля, а не значение, поэтому синтаксис остаётся управляемым и не открывает дыр для инъекций. Временную связь в runtime объявляют так же, как постоянную, но прямо в вызове:
<?php
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
$rows = ProductTable::getList([
'select' => ['ID', 'NAME', 'STORE_AMOUNT' => 'STORE.AMOUNT'],
'runtime' => [
new Reference(
'STORE',
StoreProductTable::class,
Join::on('this.ID', 'ref.PRODUCT_ID')
),
],
])->fetchAll();
Runtime-поля превращают ORM в мощный инструмент отчётности: сложные аналитические выборки, которые раньше требовали сырого SQL, теперь собираются типизированно и безопасно. Это особенно ценно в highload-проектах, где отчётов много и цена ошибки в запросе высока.
Кэширование выборок в ORM
Выборки данных — частый источник нагрузки, и D7 даёт кэширование прямо на уровне getList. Достаточно передать параметр cache с временем жизни в секундах — и результат запроса сохранится, а при повторном вызове с теми же параметрами отдастся из кэша, минуя базу.
<?php
$rows = OrdersLogTable::getList([
'select' => ['ID', 'ORDER_ID', 'STATUS'],
'filter' => ['=STATUS' => 'paid'],
'cache' => [
'ttl' => 3600, // время жизни кэша, секунд
'cache_joins' => true, // кэшировать и результаты join-ов
],
])->fetchAll();
Ключевой вопрос любого кэша — инвалидация (когда сбрасывать). В ORM кэш можно связать с тегированной инвалидацией: при изменении данных сущности через add/update/delete ORM способна сбросить связанные теги, и устаревшая выборка перестаёт отдаваться. Это тот же принцип, что и в теговом кэше компонентов: не «протухание по времени», а точечный сброс по факту изменения. Для справочных данных, которые редко меняются (валюты, характеристики, регионы), это даёт почти бесплатную скорость.
Практический ориентир такой. Кэшируйте выборки, которые (а) тяжёлые или частые и (б) меняются редко. Не кэшируйте персональные и постоянно меняющиеся данные с длинным TTL — получите «залипшие» значения. И помните: кэш ORM — это уровень запросов; над ним есть кэш компонентов и композит. Как выстроить всю пирамиду кэширования, чтобы слои не мешали друг другу, подробно разобрано в материале про кэширование в 1С-Битрикс.
Транзакции в D7
ORM отлично читает и пишет одиночные записи, но по умолчанию не оборачивает цепочку операций в транзакцию — каждый add/update/delete это отдельный запрос, который применяется сразу. Когда несколько изменений должны сработать «всё или ничего» (списать остаток и создать документ отгрузки; провести оплату и обновить статус заказа), транзакцию открывают явно через соединение с базой.
<?php
use Bitrix\Main\Application;
$connection = Application::getConnection();
$connection->startTransaction();
try {
// Несколько операций, которые должны примениться вместе
StoreProductTable::update($storeRowId, ['AMOUNT' => $newAmount]);
ShipmentTable::add([
'ORDER_ID' => $orderId,
'STATUS' => 'ready',
]);
$connection->commitTransaction(); // всё прошло — фиксируем
} catch (\Throwable $e) {
$connection->rollbackTransaction(); // ошибка — откатываем всё
throw $e; // не глотаем исключение молча
}
Схема стандартна для любой работы с транзакциями: открыли (startTransaction), выполнили операции внутри try, при успехе зафиксировали (commitTransaction), при исключении откатили (rollbackTransaction). Ключевое — не глотать исключение: после отката его нужно пробросить или залогировать, иначе вы получите молчаливую потерю данных.
Bitrix\Sale) многое уже обёрнуто внутри объектного API — там свои механизмы сохранения. Но как только вы пишете напрямую в несколько своих таблиц, ответственность за целостность на вас, и транзакция обязательна.
HighloadBlock через ORM
HighloadBlock (highload-инфоблок) — механизм Битрикса для хранения больших объёмов однотипных данных в отдельной таблице, а не в универсальной структуре обычных инфоблоков. Он с самого начала спроектирован под D7: у каждого highload-блока есть своя сущность ORM, и работа с ним — это чистая ORM, без старого API вообще.
Чтобы получить DataManager highload-блока, его сущность нужно «скомпилировать» по идентификатору блока. Дальше всё как с обычным табличным классом:
<?php
use Bitrix\Main\Loader;
use Bitrix\Highloadblock\HighloadBlockTable as HLBT;
Loader::includeModule('highloadblock');
// 1. Получаем описание highload-блока по ID
$hlblock = HLBT::getById($hlblockId)->fetch();
// 2. Компилируем сущность и берём её класс DataManager
$entity = HLBT::compileEntity($hlblock);
$dataClass = $entity->getDataClass();
// 3. Работаем как с обычным DataManager
$rows = $dataClass::getList([
'select' => ['ID', 'UF_XML_ID', 'UF_NAME'],
'filter' => ['=UF_ACTIVE' => 1],
'order' => ['UF_NAME' => 'ASC'],
'limit' => 100,
])->fetchAll();
// Добавление записи
$dataClass::add([
'UF_XML_ID' => 'sku-001',
'UF_NAME' => 'Красный',
'UF_ACTIVE' => 1,
]);
Обратите внимание: поля highload-блока — это пользовательские свойства с префиксом UF_, и обращаетесь вы к ним так же, как к любым полям ORM. Всё, что мы обсуждали, — связи, рантаймы, агрегаты, кэш, транзакции — работает и здесь, потому что это обычная сущность DataManager.
Именно поэтому highload-блоки идеальны для справочников на сотни тысяч записей, характеристик товаров, внешних каталогов и логов: отдельная таблица снимает нагрузку с универсальных таблиц инфоблоков, а типизированная ORM делает работу удобной. Когда использовать highload-блоки вместо обычных инфоблоков и как их проектировать под нагрузку — тема отдельной статьи про highload-блоки в 1С-Битрикс.
Чем D7 лучше CIBlockElement::GetList и CModule
Соберём преимущества D7 предметно, на конкретных болях старого API. Речь не про «новое ради нового», а про измеримые выигрыши в разработке и эксплуатации.
- Типизация вместо магических массивов. В
CIBlockElement::GetList($order, $filter, $group, $nav, $select)все аргументы — нетипизированные массивы со строковыми ключами и префиксами. Опечатался вPROPERTY_COLOR— узнаешь в рантайме. В ORM поля описаны, IDE подсказывает, ошибка видна раньше. - Безопасность из коробки. ORM параметризует значения автоматически. В старом API легко собрать фильтр из пользовательского ввода и получить SQL-инъекцию, если забыть экранирование. В D7 инъекции неоткуда взяться при штатном использовании.
- Читаемость. Именованные ключи
select/filter/orderпротив позиционных аргументов и префиксов>=~%!. ЧужойgetListпонятен без документации. - Связи одним запросом.
Referenceиjoinубирают N+1 и ручной SQL. В старом API связи собирались циклами выборок. - Единая обработка ошибок.
ResultсisSuccess()иgetErrorMessages()вместо разнобоя изfalse, ID и глобальных предупреждений. - Автозагрузка вместо ручных подключений. Пространства имён и автозагрузка против россыпи
CModule::IncludeModuleиrequire.Loader::includeModuleвозвращаетbool, который можно проверить. - Тестируемость. Классы, сервис-локатор и уход от глобального состояния делают код пригодным для юнит-тестов, чего почти нельзя добиться на глобальных функциях.
Честно о минусах: у D7 порог входа выше — нужно понимать ООП, namespaces и саму модель ORM. И не вся функциональность ядра имеет полноценный D7-эквивалент, поэтому местами приходится возвращаться к старому API. Но для новой логики выигрыш перевешивает почти всегда.
Таблица: старое API против D7
Чтобы держать соответствие под рукой, вот компактная «шпаргалка перевода» типичных задач со старого API на D7. Она же — карта того, что искать в D7, когда переписываете старый код.
| Задача | Старое API | D7 / ORM |
|---|---|---|
| Подключить модуль | CModule::IncludeModule('iblock') | Loader::includeModule('iblock') |
| Выбрать элементы | CIBlockElement::GetList(...) | ElementTable::getList([...]) |
| Выбрать разделы | CIBlockSection::GetList(...) | SectionTable::getList([...]) |
| Выбрать пользователей | CUser::GetList(...) | UserTable::getList([...]) |
| Дата и время | MakeTimeStamp, строки | Bitrix\Main\Type\DateTime |
| Соединение с БД | глобальный $DB | Application::getConnection() |
| Параметры запроса | $_GET, $_POST | ...getContext()->getRequest() |
| Работа с файлами | CFile | Bitrix\Main\FileTable, IO-классы |
| Результат операции | false / ID / глобаль | объект Result |
Важная оговорка: не для каждой строки соответствие «один в один». Например, сохранение элемента инфоблока со всей бизнес-логикой (события, права, пересчёт) в старом API делает CIBlockElement::Add/Update, и полноценной «сырой» ORM-замены с той же логикой у него нет — там нужен либо тот же класс, либо объектный API инфоблоков. Поэтому таблица — ориентир, а не жёсткое правило. Чтение почти всегда переводится на ORM свободно; запись сущностей ядра — осторожно.
Совместимость старого и нового кода
Главный страх при переходе на D7 — «придётся всё переписать». Не придётся. Старое и новое ядро сосуществуют в одной установке, работают с одной базой и вызываются в одном скрипте. Это позволяет мигрировать эволюционно, а не революцией.
На практике это выглядит буднично. Вы можете в одном методе выбрать данные через ORM, а сохранить через старый класс, если он даёт нужную бизнес-логику:
<?php
use Bitrix\Main\Loader;
use Bitrix\Iblock\ElementTable;
Loader::includeModule('iblock');
// Чтение — современно, через ORM
$rows = ElementTable::getList([
'select' => ['ID', 'NAME', 'CODE'],
'filter' => ['=IBLOCK_ID' => 12, '=ACTIVE' => 'Y'],
'limit' => 20,
])->fetchAll();
// Запись элемента инфоблока — через старый класс,
// чтобы отработали события, права и пересчёт свойств
$el = new \CIBlockElement();
$el->Update($rows[0]['ID'], ['NAME' => $rows[0]['NAME'] . ' (обновлён)']);
Это не «плохой код» — это прагматичное использование сильных сторон каждого слоя. Правило гигиены такое: в пределах одной логической операции старайтесь не смешивать слои без нужды (чтобы не запутать поведение событий и кэша), но в пределах проекта совмещать нормально и правильно. Читайте на D7, а пишите тем API, который даёт корректную бизнес-логику для конкретной сущности.
Отдельно про события. У D7 своя событийная модель (Bitrix\Main\EventManager, обработчики через ORM-события onBeforeAdd, onAfterUpdate и т. д.), но многие старые события продолжают работать. При смешивании важно понимать, какие обработчики сработают при вашем способе записи, — иначе можно получить «пропавший» обмен с 1С или несработавшую бизнес-логику.
Миграция со старого API: пошагово
Перевод проекта на D7 — не разовое событие, а планомерная работа. Ниже — порядок, который на практике даёт результат без остановки разработки и без риска «всё сломать одним коммитом».
- Начните с нового кода. Договоритесь в команде: вся новая логика пишется на D7. Это останавливает рост legacy, даже если старое пока не трогаете.
- Переводите чтение раньше записи. Выборки (
GetList) переносятся на ORM проще и безопаснее всего — здесь нет риска сломать бизнес-логику сохранения. Начинайте с них. - Опишите свои сущности. Для собственных таблиц заведите классы-наследники
DataManagerсgetMap(). Это разом даёт типизацию, валидацию и связи. - Выносите повторяющиеся выборки в репозитории. Оберните типовые
getListв методы сервисных классов — код перестанет дублироваться, а места для оптимизации станут очевидны. - Запись сущностей ядра переводите осторожно. Там, где есть объектный API (заказы, корзина в
Bitrix\Sale), переходите на него; где его нет — оставляйте старые классы, но изолируйте их за своим фасадом. - Настройте автозагрузку своих классов. Оформите свой код как модуль или зарегистрируйте классы, чтобы
requireисчезли и появилась предсказуемая структура. - Проверяйте события и кэш после каждого шага. Убедитесь, что обмен с 1С, права и теговый кэш продолжают работать. Миграция ORM не должна ломать инвалидацию.
Ключевая установка — никакого «большого переписывания». Проект живёт и приносит деньги; ваша задача — постепенно снижать долю legacy, начиная с самого безопасного (чтение) и оставляя рискованное (запись сущностей ядра) на потом. Через несколько итераций доля D7-кода становится преобладающей, а старое API остаётся только там, где оно действительно нужно.
Где старое API ещё оправдано
D7 — не догма. Есть места, где старое API остаётся правильным выбором, и притворяться, что это не так, — вредно. Понимание границы экономит время и уберегает от переусложнения.
- Запись элементов инфоблоков с полной логикой.
CIBlockElement::Add/Updateтянет за собой события, права, пересчёт свойств и SKU. Писать в те же таблицы «сырой» ORM в обход этого опасно — потеряете бизнес-логику. - Функциональность без D7-эквивалента. Часть возможностей модулей исторически живёт только в старых классах. Пока эквивалента нет — используйте старое API, но изолируйте его за своим интерфейсом.
- Быстрые правки в legacy-коде. В большом старом проекте иногда дешевле и безопаснее дописать в существующем стиле, чем переписывать кусок на D7 ради одной правки. Прагматизм важнее чистоты.
- Некоторые операции файлового и почтового API. Ряд утилитарных вещей проще делать старыми функциями — там нет выигрыша от ORM.
Правильная стратегия — не «D7 везде любой ценой», а «D7 по умолчанию, старое API там, где оно объективно уместно». Оборачивайте старые вызовы в собственные сервисные классы: тогда снаружи ваш код выглядит современно, а внутри использует то API, которое даёт корректное поведение. Так вы получаете и чистую архитектуру, и надёжность.
Частые ошибки
Переход на D7 порождает свой набор типовых граблей. Зная их заранее, вы избежите большинства болезненных отладок.
- «Сырая» запись в таблицы сущностей ядра. Писать в
b_iblock_elementнапрямую через свойDataManager— значит обойти события, права и пересчёты. Для элементов инфоблоков и заказов используйте специализированный API. - Забытая проверка
Loader::includeModule. Вызвали ORM-класс модуля, не подключив модуль, — получили «class not found». Всегда подключайте модуль и проверяйте результат. - Игнорирование
Result. Вызвалиadd()/update()и не проверилиisSuccess()— ошибка валидации прошла молча. Всегда читайте результат. - Проблема N+1 вместо связей. Выборка в цикле «за деталями» вместо
Referenceиjoin. Соберите всё однимgetListсо связями. - Сборка выражений из пользовательского ввода.
ExpressionFieldс сырой строкой от пользователя — вернули себе инъекцию, которую ORM обычно предотвращает. Используйте плейсхолдеры и не подставляйте ввод в SQL-выражения. - Отсутствие транзакции при связанных изменениях. Несколько зависимых
update/addбез транзакции — при сбое посередине данные рассинхронизируются. Оборачивайте вstartTransaction/commit/rollback. - Кэш выборок без инвалидации. Включили
cacheс большим TTL на изменяемые данные и забыли про сброс — пользователи видят устаревшее. Связывайте кэш с тегами или ставьте разумный TTL. - Смешение слоёв в одной операции. Часть записи через ORM, часть через старый класс в одной транзакции логики — путаница в событиях и кэше. Держите одну операцию в одном слое.
- Ручные
requireвместо автозагрузки. Подключение файлов классов вручную ломает предсказуемость. Регистрируйте свои классы в автозагрузке.
Чек-лист перехода на D7
Короткий список, по которому удобно свериться при разработке и код-ревью нового кода на Битриксе:
- Модули подключаются через
Loader::includeModule()с проверкой результата, а не «на удачу». - Классы используются через пространства имён и автозагрузку; ручных
requireдля ядра нет. - Чтение данных идёт через ORM
getListс понятнымиselect/filter/order, а не черезCIBlockElement::GetList. - Свои таблицы описаны классами-наследниками
DataManagerс типизированнымgetMap(). - Связи между сущностями сделаны через
Referenceиjoin; проблемы N+1 нет. - Результаты
add/update/deleteпроверяются черезResult::isSuccess(). - Связанные изменения обёрнуты в транзакцию (
startTransaction/commit/rollback) с пробросом исключения. - Тяжёлые редко меняющиеся выборки кэшируются (
cache) с продуманной инвалидацией. - Highload-блоки используются через ORM (
compileEntity→getDataClass). - Запись сущностей ядра идёт через объектный/старый API, а не «сырой» ORM в обход событий.
- Старое API, где оно оставлено, изолировано за своим сервисным классом.
Вывод
Ядро D7 — это не «другой Битрикс», а современный слой той же платформы: пространства имён и автозагрузка вместо ручных подключений, Application и сервис-локатор вместо глобальных объектов, ORM с DataManager вместо «магических массивов» старого API. Чтение данных через getList становится типизированным, читаемым и безопасным; связи через Reference убирают N+1; рантайм-поля и агрегаты собирают сложные отчёты одним запросом; кэш и транзакции решают вопросы скорости и целостности. HighloadBlock изначально живёт на ORM и закрывает задачи больших справочников.
Практический стержень — эволюция, а не революция. Старое и новое ядро сосуществуют, поэтому мигрировать можно постепенно: новый код на D7, чтение переводим раньше записи, свои таблицы описываем сущностями, а старое API оставляем там, где оно даёт корректную бизнес-логику. Так проект становится чище, безопаснее и быстрее в разработке, не теряя стабильности. Если нужен такой код под ваш проект — мы пишем его на D7 в рамках highload-проектов и разработки REST API для 1С-Битрикс. Смежные материалы по архитектуре — REST API в 1С-Битрикс, highload-блоки и вся рубрика «Разработка и архитектура».