Как повесить onBeforeAdd на DataManager в 1С-Битрикс и отменить add через addError?

OrderLogTable уже есть, add() отработал - а CREATED_AT пустой, PRICE с нулём проходит, на форуме советуют "зарегистрируй в init.php". Для onBeforeAdd внутри *Table регистрация не нужна: ядро вызовет метод само. Ниже - 9 событий D7 ORM, EventResult с modifyFields и addError, отличие от AddEventHandler. Вы повесите onBeforeAdd, отмените невалидную запись и подпишетесь на сущность снаружи.

События DataManager - это хуки внутри жизненного цикла add/update/delete. onBeforeAdd и onBeforeUpdate принимают Entity\Event и возвращают EventResult: modifyFields нормализует поля, addError останавливает операцию. Метод onBeforeAdd пишется прямо в *Table, имя строго onBeforeAdd, не onBeforeItemAdd. Для DateTimeField нужен объект Bitrix\Main\Type\DateTime, не строка date(). Внешняя подписка - через ORM\EventManager на класс сущности, это не то же самое, что AddEventHandler для iblock или sale.

Материал про коробочную CMS "1С-Битрикс: Управление сайтом", не про облачный Bitrix24. ORM (Object-Relational Mapping) - слой D7, где PHP-класс *Table связан с таблицей MySQL. Если таблицы ещё нет - сначала создайте *Table и getMap, затем возвращайтесь сюда за событиями.

Выберите, когда нужны события D7 ORM, а когда хватит CRUD или AddEventHandler

Сравнение: когда нужны события D7 ORM, а когда CRUD или AddEventHandler

Бизнес-правила можно размазать по контроллерам, админке и компонентам - но тогда каждый новый вход в данные обходит проверку. События *Table возвращают контроль туда, где живут поля.

Задача CRUD без хуков (B71) Validators в getMap onBeforeAdd в *Table AddEventHandler модуля (B58)
Простая запись лога как есть Да Избыточно Нет Нет
Длина строки, regex, диапазон числа Нет - дубли в коде Да - RegExp, Range в getMap Только если правило зависит от контекста Нет
PRICE > 0, автозаполнение CREATED_AT Ненадёжно Частично Да - addError + modifyFields Нет - другой API
Реакция на сохранение чужого инфоблока Нет Нет Нет - нужен ElementTable или B58 Да - OnBeforeIBlockElementAdd
Логика в другом модуле без fork *Table Нет Нет Через ORM\EventManager снаружи Для ORM-сущности - не ядро EventManager

Модульные обработчики в init.php разобраны в гайде по AddEventHandler. Highload-справочники с похожим API событий - в материале про HL CRUD. Каркас модуля, куда вешать внешнюю подписку в include.php, - в инструкции по своему модулю.

Делайте: держите проверки данных в onBefore*, побочные эффекты (почта, кеш) в onAfter*. Не делайте: не путайте OnBeforeAdd ORM с AddEventHandler('iblock', ...) - разные менеджеры и разные параметры события.

Разберите карту 9 событий DataManager: onBefore, on, onAfter

Карта 9 событий DataManager: onBefore, on, onAfter

У каждой операции add, update и delete - три фазы. Ядро документирует их в операциях с сущностями ORM.

Схема add():
OnBeforeAdd (modifyFields, addError, unsetFields) → OnAdd → OnAfterAdd (уведомления, кеш, лог без тяжёлого SQL)
Событие Когда вызывается Что можно сделать
OnBeforeAdd / OnBeforeUpdate До INSERT/UPDATE modifyFields, addError, unsetFields - запись можно отменить
OnAdd / OnUpdate В момент записи Редко нужны в прикладном коде
OnAfterAdd / OnAfterUpdate / OnAfterDelete После успешной операции Очистка кеша, очередь, письмо - без N+1 запросов

EventResult::modifyFields и unsetFields работают только в onBefore-фазе. addError на onBefore прерывает add() или update(): Result::isSuccess() вернёт false, INSERT не выполнится.

Делайте: тяжёлую выборку связанных строк выносите из onAfterAdd в отдельный сервис или агент. Не делайте: не кладите в onAfterAdd цикл с getList по каждой связанной таблице - получите N+1 и тормоза на массовом импорте.

Настройте onBeforeAdd и onBeforeUpdate в классе *Table

Чеклист настройки onBeforeAdd и onBeforeUpdate в классе *Table

Типичная ошибка с форума dev.1c-bitrix.ru: метод назван onBeforeItemAdd или modifyFields передали как ['fields' => ['CREATED_AT' => ...]]. mergeObjectFields ждёт плоский массив полей сущности, без вложенного ключа fields.

  1. Откройте OrderLogTable.php в lib/ модуля (после создания *Table).
  2. Добавьте статический метод onBeforeAdd с сигнатурой public static function onBeforeAdd(\Bitrix\Main\Entity\Event $event).
  3. Создайте $result = new \Bitrix\Main\Entity\EventResult() и прочитайте $fields = $event->getParameter('fields').
  4. Проверьте PRICE: при значении <= 0 вызовите $result->addError(new EntityError('...')) и return $result.
  5. Нормализуйте CREATED_AT через $result->modifyFields(['CREATED_AT' => new \Bitrix\Main\Type\DateTime()]).
  6. Протестируйте OrderLogTable::add(['PRICE' => 0]) - isSuccess() false, текст ошибки из addError.
public static function onBeforeAdd(\Bitrix\Main\Entity\Event $event)
{
    $result = new \Bitrix\Main\Entity\EventResult();
    $fields = $event->getParameter('fields');

    if (isset($fields['PRICE']) && (float)$fields['PRICE'] <= 0) {
        $result->addError(
            new \Bitrix\Main\Entity\EntityError('PRICE должен быть больше нуля')
        );
        return $result;
    }

    $result->modifyFields([
        'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
    ]);

    return $result;
}

Для onBeforeUpdate тот же шаблон: unsetFields запрещает менять поле (например ISBN после публикации), FieldError привязывает ошибку к конкретному полю. В старом API правили $arFields по ссылке; в D7 ORM меняют только через EventResult.

Делайте: возвращайте EventResult из каждого onBefore-метода. Не делайте: не передавайте в modifyFields строку date('Y-m-d H:i:s') для DatetimeField - ядро ожидает Type\DateTime.

Сравните validators в getMap и бизнес-правила в onBefore

Встроенные валидаторы (Length, RegExp, Range, Unique) в getMap срабатывают при checkFields до записи - удобно для статичных ограничений. Правило "PRICE не ниже закупки из связанной таблицы" или "запрет смены статуса без роли" - в onBeforeUpdate, иначе дублируете closure в add и update.

  • getMap + validators: формат email, длина артикула, уникальность кода в таблице.
  • onBeforeAdd/onBeforeUpdate: контекст пользователя, связанные сущности, автозаполнение служебных полей.
  • onAfter*: побочные эффекты после успешной записи, не валидация.

Делайте: одно бизнес-правило - одно место в onBefore*. Не делайте: не копируйте одну и ту же проверку в компонент, REST и *Table - пропустите путь, данные протекут.

Подпишитесь на событие снаружи через ORM\EventManager

Когда правку *Table в чужом модуле делать нельзя, подписка в include.php своего модуля:

use Bitrix\Main\ORM\EventManager;
use Bitrix\Main\ORM\Data\DataManager;
use Mycompany\Custommodule\OrderLogTable;

EventManager::getInstance()->addEventHandler(
    OrderLogTable::class,
    DataManager::EVENT_ON_BEFORE_ADD,
    ['\\Mycompany\\Custommodule\\Handler', 'onBeforeOrderLogAdd']
);

Имя события - константа EVENT_ON_BEFORE_ADD, класс - OrderLogTable::class, без суффикса Table в строке события. Обработчик получает тот же Entity\Event и возвращает EventResult с modifyFields или addError.

Делайте: в uninstall или при отключении модуля вызывайте removeEventHandler. Не делайте: не регистрируйте один и тот же обработчик при каждом хите init.php без проверки - дубли сработают.

Пройдите чек-лист перед выкладкой на prod

  1. Проверьте имя метода: onBeforeAdd, onBeforeUpdate, onAfterAdd - регистр и префикс on.
  2. Убедитесь, что add() вызывается на том же *Table, где лежит обработчик.
  3. Прогоните add/update/delete на staging: isSuccess(), тексты ошибок, автозаполнение полей.
  4. Сделайте onAfter-обработчики идемпотентными: ядро может отправить событие дважды (legacy Entity\Event и ORM\Event).
  5. Оставьте одну ответственность на событие - валидация в onBefore, уведомление в onAfter.

Нужна помощь с внедрением событий в боевой модуль - обсудим задачу. Похожие кейсы по Bitrix - в портфолио.

Автор: Максим Мольков, разработчик 1С-Битрикс.
Источники: документация ORM entity-operations, API DataManager, урок 2244, форум topic120789.

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

Чем OnBeforeAdd в DataManager отличается от AddEventHandler?

OnBeforeAdd - статический метод в *Table или подписка ORM\EventManager на класс сущности; параметр fields, ответ EventResult. AddEventHandler вешает обработчик на событие модуля (iblock, sale) через ядро Bitrix\Main\EventManager - другой API, без modifyFields. Для своей таблицы модуля нужен ORM-путь, не B58.

Как отменить add() через addError?

В onBeforeAdd создайте EventResult, вызовите addError(new EntityError('текст')) и верните $result. DataManager::add() завершится с isSuccess() = false, массив ошибок в getErrorMessages(), строка в БД не появится.

Зачем modifyFields вместо правки $arFields по ссылке?

В D7 ORM поля приходят в Event как параметр fields, а не как &$arFields старого API. Изменения разрешены только через EventResult::modifyFields с плоским массивом ['FIELD' => value]. Вложенный ключ fields ломает mergeObjectFields.

Можно ли повесить onBeforeAdd на HighloadBlockTable?

Да. У скомпилированной HL-сущности тот же набор из 9 событий DataManager. Подписка на DataClass HL или метод в классе сущности - по тому же контракту Event + EventResult. Подробнее про HL - в гайде B78.

Почему onAfterAdd срабатывает дважды при отладке?

В DataManager::add() ядро отправляет событие и в legacy Entity\Event, и в ORM\Event. Обработчик без идемпотентности выполнит побочный эффект дважды. Добавьте флаг, проверку по ID записи или перенесите логику в одно место с защитой от повтора.

Нужно ли регистрировать onBeforeAdd в init.php?

Нет, если метод объявлен внутри *Table - ядро находит его автоматически. init.php нужен только для внешней подписки через ORM\EventManager::addEventHandler на чужую или свою сущность из другого файла.

Обсудить вашу задачу

Было полезно?

Поставьте лайк, если статья помогла разобраться.

Поделиться статьёй

Отправьте тому, кому это пригодится.

Telegram ↗ MAX ↗ ВКонтакте ↗ Одноклассники ↗
Все статьи ↗

Как настроить регистрацию и личный кабинет покупателя на 1С-Битрикс?

Структура шаблонов сайта в Битриксе и поиск header footer

Как настроить скидки и промокоды на 1С-Битрикс: правила корзины и купоны