На практике типичная ситуация: Highload-блок с городами уже создан в админке, но на проде в шаблоне всплывает Class not found, а getList возвращает пустой массив. Например, в git остался HLBLOCK_ID=3 с локалки — это проблема переноса между окружениями: в агенте compileEntity вызывают в каждой итерации цикла, а add() с пустым UF_CITY молча не сохраняет запись. Ниже — рабочая цепочка D7 ORM для CMS: от compileEntity до add/update/delete с isSuccess(), фабрика entityClass без N+1 и переносимый код по NAME вместо жёсткого ID.
Highload в коде - это не админка, а DataClass после compileEntity. Один HLBlockFactory на проект кэширует класс на HTTP-хит, resolveHighloadblock('Cities') переживает деплой без смены ID, getList/add/update/delete проверяют isSuccess() и getErrorMessages(). Удаление записи - только $entityClass::delete($id), не HighloadBlockTable::delete.
Материал про коробочную 1С-Битрикс: Управление сайтом. Облачный Bitrix24 со своими сущностями - другой продукт, здесь только модуль highloadblock. Создание HL-блока в админке уже разобрано в гайде по Highload - эта статья начинается там, где блок уже есть, и нужен PHP-код.
Определите, когда нужен HL ORM в коде, а когда хватит инфоблока или своей таблицы
Highload-блок - справочник или лог без иерархии разделов: города, статусы заказов, коды складов. Данные лежат в отдельной таблице с полями UF_*, а PHP-класс для CRUD ядро собирает на лету через compileEntity. Если задача - новости, каталог с SEO или дерево разделов, берите инфоблок D7. Если нужна полностью своя схема под модуль - своя *Table в lib/.
| Задача | Инфоблок D7 | Highload ORM | Своя *Table в модуле | Только админка |
|---|---|---|---|---|
| Справочник 20 000 городов | Медленно, лишние сущности | Да - отдельная таблица HL | Избыточно | Нет API в коде |
| Новости и статьи блога | Да - ElementTable | Нет | Нет | Редко |
| Очередь задач модуля | Нет | Иногда | Да - полный контроль | Нет |
| Редактирование менеджером в админке | Да | Да - HL-список в админке | Нет UI | Да, без PHP |
Выборка инфоблока - в сравнении CIBlockElement и D7 ORM, своя таблица DataManager - в гайде по *Table в модуле. HL ORM подключают, когда справочник уже в Highload и нужен getList/add из компонента, агента или REST.
Делайте: храните плоские справочники в HL и работайте через DataClass. Не делайте: не тащите 50 000 SKU в инфоблок только ради привычки - нагрузка на индексы и кеш вырастет.
Получите DataClass через compileEntity и вынесите фабрику в lib/ модуля
ORM (Object-Relational Mapping) в D7 - слой, который связывает PHP-класс с таблицей MySQL. Для HL класс не пишут руками: HighloadBlockTable::compileEntity() собирает DataManager с полями UF_* и возвращает имя класса через getDataClass(). Повторный compileEntity на одном хите может пересоздать класс через eval - держите результат в static cache.
- Подключите модуль: Loader::includeModule('highloadblock').
- Найдите блок по символьному NAME через HighloadBlockTable::resolveHighloadblock('Cities') или по TABLE_NAME в getList.
- Скомпилируйте сущность: $entity = HighloadBlockTable::compileEntity($hlblock); $dataClass = $entity->getDataClass().
- Вынесите логику в HLBlockFactory в lib/ кастомного модуля - см. создание модуля.
- Закэшируйте $dataClass в static-массиве по NAME или TABLE_NAME на весь HTTP-запрос.
- Проверьте на staging: тот же код без правки HLBLOCK_ID после деплоя с локалки.
<?php
use Bitrix\Main\Loader;
use Bitrix\Highloadblock\HighloadBlockTable;
class HLBlockFactory
{
private static array $cache = [];
public static function getDataClass(string $hlName): string
{
if (isset(self::$cache[$hlName])) {
return self::$cache[$hlName];
}
Loader::includeModule('highloadblock');
$hlblock = HighloadBlockTable::resolveHighloadblock($hlName);
if (!$hlblock) {
throw new \RuntimeException('HL block not found: ' . $hlName);
}
$entity = HighloadBlockTable::compileEntity($hlblock);
return self::$cache[$hlName] = $entity->getDataClass();
}
}
Схема: resolveHighloadblock → compileEntity → getDataClass → static cache → единый сервис в lib/.
Делайте: один getDataClass() на проект. Не делайте: не копируйте compileEntity в result_modifier, агент и REST - Class not found на проде почти гарантирован.
Настройте getList: select, filter, order и проверьте пустую выборку
После получения $dataClass выборка идёт как у любого DataManager: getList с ключами select, filter, order, limit. Поля пользователя всегда с префиксом UF_: UF_NAME, UF_ACTIVE. Фильтр по точному совпадению - '=UF_ACTIVE' => 'Y', по подстроке - '%UF_NAME' => 'Моск'.
$dataClass = HLBlockFactory::getDataClass('Cities');
$rows = $dataClass::getList([
'select' => ['ID', 'UF_NAME', 'UF_REGION'],
'filter' => ['=UF_ACTIVE' => 'Y'],
'order' => ['UF_NAME' => 'ASC'],
'limit' => 50,
]);
while ($row = $rows->fetch()) {
// $row['UF_NAME']
}
Если массив пустой при "правильном" коде, проверьте TABLE_NAME, префикс UF_ в filter/select и значение UF_ACTIVE (Y/N). Class ... was not found обычно значит неверный ID на окружении - переходите на resolveHighloadblock. Связь двух HL - через runtime Reference в getList, см. документацию highloadblock.
Делайте: явный select только нужных UF_*. Не делайте: не вызывайте compileEntity внутри while по офисам - CPU и лишний destroy класса.
Выполните add, update и delete с проверкой isSuccess()
Запись и изменение принимают массив полей с UF_*. Методы возвращают Result. Без isSuccess() ошибка по обязательному UF-полю проглатывается, и кажется, что "ничего не произошло".
$dataClass = HLBlockFactory::getDataClass('Cities');
$result = $dataClass::add([
'UF_NAME' => 'Казань',
'UF_ACTIVE' => 'Y',
]);
if (!$result->isSuccess()) {
throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}
$newId = $result->getId();
$dataClass::update($newId, ['UF_NAME' => 'Казань (Татарстан)']);
$del = $dataClass::delete($newId);
if (!$del->isSuccess()) {
// логируем getErrorMessages()
}
Критично: HighloadBlockTable::delete($id) удаляет весь HL-блок со всеми записями. Строку справочника удаляют только через $dataClass::delete($id). Путаница описана в справочнике delete для HighloadBlockTable.
На события HL (OnBeforeAdd, OnAfterUpdate) можно повесить валидацию с ENTITY_ID из compileEntityId. После записи сбросьте кеш компонента.
Делайте: всегда обрабатывайте getErrorMessages(). Не делайте: не вызывайте HighloadBlockTable::delete, когда нужно убрать одну строку.
Различите compileEntityId и getDataClass для UF-полей
С версии 20.0.0 HighloadBlockTable::compileEntityId($id) возвращает ENTITY_ID для CUserTypeEntity при программном добавлении UF в HL. Это не getDataClass: compileEntityId - для миграции полей, compileEntity - для CRUD.
Делайте: фиксируйте обязательные UF_ в документации модуля. Не делайте: не путайте compileEntityId с именем DataClass.
Проверьте чек-лист перед выкладкой на prod
После настройки CRUD прогоните сценарий на staging и убедитесь, что код переносится без правки ID.
- Один HLBlockFactory или сервис в lib/, нет дублей compileEntity в шаблонах.
- resolveHighloadblock по символьному NAME вместо HLBLOCK_ID из локалки.
- getList возвращает ожидаемые UF_* с корректным filter.
- add/update с isSuccess() и понятным логом getErrorMessages().
- delete только через DataClass, не HighloadBlockTable::delete.
- Кеш компонента сброшен после изменения справочника.
Вы получите переносимый слой доступа к HL: один хелпер, предсказуемый CRUD и понятные ошибки вместо Class not found на проде. Если нужен аудит Highload на действующем проекте - обсудим задачу, примеры внедрений - в портфолио.
Автор: Максим Мольков, разработчик 1С-Битрикс.
Источники: документация D7 highloadblock, HighloadBlockTable::compileEntity, практика внедрений на CMS.
Частые вопросы
Чем Highload ORM отличается от инфоблока D7?
Инфоблок - контент с разделами, SEO и ElementTable. Highload - плоский справочник в отдельной таблице с UF_* и compileEntity. Для каталога статей берите инфоблок, для 20 000 городов - HL. Подробнее в сравнении GetList и D7 ORM.
Как получить DataClass по ID HL-блока?
Loader::includeModule('highloadblock'), затем $hl = HighloadBlockTable::getById($id)->fetch(), $entity = HighloadBlockTable::compileEntity($hl), $dataClass = $entity->getDataClass(). Надёжнее на prod - resolveHighloadblock('SymbolicName') без привязки к числовому ID.
Почему add() возвращает ошибку по UF-полю?
Проверьте обязательные UF_*, тип поля и формат значения. Вызовите getErrorMessages() на Result. Пустой UF_CITY для обязательного поля - типичная причина "тихого" отказа без isSuccess().
Как удалить запись Highload программно?
Получите $dataClass через фабрику, вызовите $dataClass::delete($elementId) и проверьте isSuccess(). HighloadBlockTable::delete удаляет весь блок - для одной строки справочника он не подходит.
Можно ли повесить обработчик на HL-блок как на инфоблок?
Да, события OnBeforeAdd, OnAfterUpdate и аналоги для HL существуют. Регистрируйте обработчик на модуль highloadblock с ENTITY_ID из compileEntityId. Логику валидации держите в одном месте, не дублируйте в каждом add().
Почему на prod Class not found, а на локалке работало?
Чаще всего другой HLBLOCK_ID или TABLE_NAME на окружении. Уберите жёсткий getById(3), перейдите на resolveHighloadblock по NAME и static cache compileEntity. Создание самого блока - в гайде по созданию Highload.