До 10%Рекомендуйте нас и получайте процент за каждого приведённого клиента

События и обработчики в 1С-Битрикс: как расширять систему без правки ядра

События и обработчики в 1С-Битрикс: механизм событий, регистрация обработчиков в init.php, AddEventHandler и EventManager D7, изменение поведения заказа и каталога без правки ядра

Заказчик просит: «Пусть при оформлении заказа с самовывозом автоматически ставится комментарий для склада, а после смены статуса на „Оплачен“ уходит задача менеджеру». Задача житейская, но новичок на 1С-Битрикс сразу тянется править компонент оформления заказа или, того хуже, лезет в файлы модуля sale в папке bitrix. И то и другое — тупик: компонент отвечает за вывод, а не за бизнес-логику сохранения, а правка ядра исчезнет при первом обновлении платформы. Правильный инструмент здесь один — события.

Механизм событий (event handlers) — это официальная, поддерживаемая точка расширения Битрикса. Платформа в ключевых местах сама «зовёт» ваш код: перед добавлением пользователя, перед и после обновления товара, при сохранении заказа, при изменении корзины. Вы подписываете на нужный момент свою функцию — обработчик — и меняете поведение системы, ничего не ломая в её исходниках. Эта статья — практический разбор механизма: зачем он нужен, где регистрировать обработчики (init.php, local/php_interface), чем AddEventHandler отличается от D7-го addEventHandler, какие бывают события Битрикс у модулей и у ORM, как отменять действия, задавать приоритет и не уронить скорость. Материал — выжимка из наших проектов по разработке высоконагруженных проектов на 1С-Битрикс.

Коротко

  • События — штатный способ менять поведение платформы, не правя ядро: ядро само вызывает ваш обработчик в нужный момент.
  • Регистрируют обработчики в init.php (/local/php_interface/init.php), а тело логики выносят в отдельный класс.
  • Два API: старый AddEventHandler и D7-й EventManager::addEventHandler — для модульных событий взаимозаменяемы.
  • Есть события модулей (OnBeforeUserAdd, OnAfterIBlockElementAdd, OnSaleOrderSaved) и события ORM (onBeforeAdd, onAfterUpdate).
  • before-события умеют отменить действие (return false или исключение), after-события — только реагировать; тяжёлое из обработчика откладывают.

Зачем нужны события: менять поведение, не трогая ядро

Главная ценность механизма событий формулируется одной фразой: расширять систему, не трогая её код. Ядро Битрикса — это сотни тысяч строк в папке bitrix/modules, и они принадлежат платформе. Любая правка там держится ровно до ближайшего обновления, после которого затирается штатными файлами. Значит, «вписать» свою логику прямо в чужой код нельзя — нужна легальная точка, куда платформа сама пустит ваш код. Такой точкой и служат события.

Подумайте, что обычно требует бизнес от магазина: при регистрации клиента завести его в CRM, перед сохранением товара проверить уникальность артикула, после оплаты заказа списать бонусы и отправить чек, при смене статуса уведомить склад. Всё это — не про вывод (компоненты) и не про хранение (инфоблоки), а про реакцию на действия. Событие — это именно «крючок» на действие: система сообщает «сейчас произойдёт X» или «X только что произошёл», а вы решаете, что с этим делать.

Практических выигрышей у такого подхода три:

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

Как ядро отдаёт управление вашему коду Ядро Битрикса сохраняет заказ, товар, пользователя Событие OnBeforeIBlock… точка вызова Обработчик ваша функция из init.php Действие X сделано Обработчик может: читать, менять данные, отменить действие (только в before) или запустить своё
Схема: в ходе действия ядро вызывает событие, платформа находит подписанные обработчики и передаёт им управление вместе с данными, а before-обработчик может ещё и отменить действие.

Как устроен механизм событий в Битриксе

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

Событие характеризуется двумя вещами: модулем-источником (например, main, iblock, sale, catalog) и именем события (например, OnBeforeUserAdd). Именно эта пара — «модуль + имя» — используется и при регистрации обработчика, и при вызове события. Ошибётесь в любом из двух — и подписка просто «повиснет в воздухе»: платформа не найдёт совпадения и ваш код не выполнится.

Исторически в Битриксе сложились два поколения событий, и это важно понимать, чтобы не путаться:

Хорошая новость: для большинства «модульных» событий (тех, что через ExecuteModuleEventEx) оба API регистрации — старый и новый — работают одинаково, и обработчик, подписанный любым из них, будет вызван. А вот события ORM и часть D7-событий существуют только в новом механизме. Дальше разберём оба API и покажем, когда какой уместен.

Где регистрировать: init.php и local/php_interface

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

Расположений у init.php два, и выбор между ними — это выбор между «современно» и «по-старому»:

ПутьКогда используется
/local/php_interface/init.phpСовременный проект: весь кастомный код в папке /local/, отделён от ядра
/bitrix/php_interface/init.phpКлассическое расположение; работает всегда, но мешает «своё» с системным

Если в проекте есть папка /local/, регистрируйте обработчики в /local/php_interface/init.php — это чище: весь ваш код лежит отдельно от bitrix, проект легко переносить и обновлять. Если оба файла присутствуют, подключаются оба, но смешивать регистрации в двух местах — путь к путанице. Выберите одно место и держитесь его.

Важный нюанс: init.php выполняется на каждом хите — и на публичке, и в админке, и при API-запросах, и при обмене с 1С. Поэтому в самом файле не должно быть тяжёлой работы: только регистрация обработчиков и, максимум, подключение автозагрузки. Никаких запросов к базе «на всякий случай» — иначе вы замедлите вообще всё.

Типичный аккуратный init.php выглядит так — он только регистрирует, а логику держит в классе:

<?php
// /local/php_interface/init.php
use Bitrix\Main\Loader;

// Подключаем автозагрузку своих классов (если не через composer)
require_once __DIR__ . '/lib/OrderHandlers.php';

// Регистрация обработчиков — по одной строке на подписку
$eventManager = \Bitrix\Main\EventManager::getInstance();

$eventManager->addEventHandler(
    'iblock',
    'OnBeforeIBlockElementUpdate',
    ['\App\Handlers\CatalogHandlers', 'onBeforeElementUpdate']
);

$eventManager->addEventHandler(
    'sale',
    'OnSaleOrderSaved',
    ['\App\Handlers\OrderHandlers', 'onOrderSaved']
);

Заметьте: init.php не содержит бизнес-логики — только «кто на что подписан». Всё тело обработчиков живёт в классах CatalogHandlers и OrderHandlers. Это и есть здоровая структура, к которой мы ещё вернёмся в разделе про вынос тела обработчика.

init.php — единая точка подписки на события Каждый хит публичка, админка, API local/php_interface /init.php Реестр событий модуль + имя → метод В init.php — только регистрация addEventHandler(...) Тело — в классах App\Handlers\OrderHandlers Отключить логику = убрать одну строку регистрации, ядро не тронуто
Схема: платформа на каждом хите подключает init.php, тот заполняет реестр событий, а тела обработчиков живут в отдельных классах — отключение сводится к удалению одной строки.

AddEventHandler против EventManager D7

Теперь к двум API регистрации. Оба делают одно и то же — заносят вашу функцию в реестр событий, — но выглядят по-разному и появились в разное время. Разработчику важно уметь читать и писать оба: в живых проектах встречаются и старый, и новый стиль.

Старый способ — функция AddEventHandler. Процедурная, из ранних версий Битрикса. Сигнатура: модуль, имя события, обработчик, сортировка. Обработчиком может быть имя функции строкой или массив [класс, метод].

<?php
// Старый API — глобальная функция
AddEventHandler(
    'main',                 // модуль-источник
    'OnBeforeUserAdd',      // имя события
    'checkUserBeforeAdd',   // имя функции-обработчика
    100                     // сортировка (по умолчанию 100)
);

function checkUserBeforeAdd(&$arFields)
{
    // $arFields передан по ссылке — можно менять
    if (empty($arFields['LOGIN'])) {
        global $APPLICATION;
        $APPLICATION->throwException('Логин обязателен');
        return false; // отменяем добавление
    }
    return true;
}

Новый способ — EventManager::addEventHandler. Объектный интерфейс из D7. Получаете экземпляр менеджера событий и подписываете обработчик. Стиль тот же, но в духе пространств имён и классов.

<?php
use Bitrix\Main\EventManager;

$em = EventManager::getInstance();
$em->addEventHandler(
    'main',
    'OnBeforeUserAdd',
    ['\App\Handlers\UserHandlers', 'onBeforeUserAdd']
);

Для событий, которые ядро вызывает через ExecuteModuleEventEx (а это подавляющее большинство «модульных» событий), оба варианта равнозначны — обработчик отработает независимо от того, каким API вы подписались. Разница проявляется на «настоящих» D7-событиях (send/Event), включая события ORM: их регистрируют только через addEventHandler, и обработчик получает на вход объект Event, а не набор параметров по ссылке. Сведём в таблицу.

ПризнакAddEventHandler (старый)EventManager::addEventHandler (D7)
СтильГлобальная функцияОбъект + метод
Что на входе обработчикаПараметры по ссылке (&$arFields)Параметры по ссылке или объект Event
Модульные события (ExecuteModuleEventEx)ДаДа
Настоящие D7-события (send)НетДа
События ORM (onBeforeAdd…)НетДа
Отмена действияreturn false / throwExceptionЧерез EventResult / исключение

Практический совет для новых проектов: используйте D7-й EventManager::addEventHandler — он единообразно подписывает и старые, и новые события, лучше ложится на классовую архитектуру и не заставляет держать в голове две разные функции. Старый AddEventHandler вы всё равно будете встречать в унаследованном коде, поэтому читать его надо уметь, но писать новое лучше в D7-стиле. Про то, как события ORM вписаны в общую модель данных D7, мы подробно рассказываем в статье про D7 ORM в 1С-Битрикс.

Анатомия обработчика: что приходит на вход

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

Старые модульные события передают параметры, как правило, по ссылке. Например, OnBeforeIBlockElementUpdate отдаёт массив полей элемента &$arFields — вы можете прочитать его и, что важно, изменить прямо в обработчике: правка полей повлияет на то, что реально сохранится. Возврат false из before-обработчика отменяет действие.

<?php
// Обработчик старого события: параметры по ссылке
public static function onBeforeElementUpdate(&$arFields)
{
    // Нормализуем артикул перед сохранением
    if (isset($arFields['PROPERTY_VALUES']['ARTICLE'])) {
        // ... привести к верхнему регистру, убрать пробелы ...
    }

    // Запрет пустого названия — отменяем обновление
    if (isset($arFields['NAME']) && trim($arFields['NAME']) === '') {
        global $APPLICATION;
        $APPLICATION->throwException('Название товара не может быть пустым');
        return false;
    }
    return true;
}

Новые D7-события передают в обработчик один аргумент — объект \Bitrix\Main\Event. Данные достаются из него методом getParameter(), а «ответ» обработчика (в том числе отмену) возвращают через EventResult. Это единообразнее, но требует другого кода:

<?php
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;

public static function onOrderSavedD7(Event $event)
{
    /** @var \Bitrix\Sale\Order $order */
    $order = $event->getParameter('ENTITY');
    $orderId = $order->getId();
    // ... ваша логика ...

    return new EventResult(EventResult::SUCCESS);
}

Отсюда правило, которое спасает часы отладки: сигнатура обработчика должна соответствовать поколению события. Подпишете D7-обработчик с параметром Event $event на старое событие, отдающее &$arFields, — и получите либо ошибку, либо тихо не работающую логику. Всегда сверяйтесь с документацией конкретного события: что оно передаёт и как отменяется.

Куда выносить тело обработчика

Соблазн написать логику прямо в init.php велик, но это дорога к файлу на две тысячи строк, в котором невозможно разобраться. Здоровый подход — разделять регистрацию и реализацию: в init.php только строки addEventHandler, а тела обработчиков — в классах, сгруппированных по смыслу.

Практическая схема, которая хорошо работает на проектах:

  1. Заведите папку для своих классов. Например, /local/php_interface/lib/Handlers/ или отдельный модуль в /local/modules/.
  2. Сгруппируйте обработчики по домену. OrderHandlers — всё про заказы, CatalogHandlers — про товары, UserHandlers — про пользователей. Один класс — одна предметная область.
  3. Делайте методы статическими. Обработчики обычно не хранят состояние, а статический метод удобно указать в регистрации массивом [класс, метод].
  4. Подключите автозагрузку. Через composer PSR-4 или простой Loader::registerAutoLoadClasses в init.php — чтобы классы находились без ручных require.
  5. В init.php оставьте только подписки. Прочитав его, любой разработчик за минуту видит полную карту «что на что подписано».
<?php
// /local/php_interface/lib/Handlers/OrderHandlers.php
namespace App\Handlers;

class OrderHandlers
{
    public static function onOrderSaved(\Bitrix\Main\Event $event)
    {
        // Вся логика реакции на сохранение заказа — здесь
    }

    public static function onStatusChanged(\Bitrix\Main\Event $event)
    {
        // Реакция на смену статуса — отдельным методом
    }
}

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

До и после: before- и after-события

Почти у каждого действия в Битриксе есть два события-близнеца: before (перед действием) и after (после). Разница между ними принципиальна и определяет, что вы вообще можете сделать в обработчике. Спутать их — классическая ошибка, из-за которой «код вроде правильный, а не работает».

Before-событие вызывается до того, как действие совершилось. В этот момент данные ещё не записаны, поэтому обработчик может: проверить их, изменить (правка полей повлияет на итог) и — главное — отменить действие. Именно before используют для валидации, нормализации и запретов. Пример: OnBeforeIBlockElementUpdate — можно поправить артикул или запретить сохранение с пустым названием.

After-событие вызывается после того, как действие уже произошло и данные записаны. Отменить его нельзя — поезд ушёл. Зато на входе доступен реальный результат, в том числе новый ID у только что созданной сущности. After используют для реакции: отправить уведомление, поставить задачу, синхронизировать с внешней системой. Пример: OnAfterIBlockElementAdd — товар уже создан, знаем его ID, можно завести его в стороннем сервисе.

Аспектbefore-событиеafter-событие
Момент вызоваДо записи данныхПосле записи данных
Можно менять данныеДа (правка повлияет на итог)Смысла нет — уже сохранено
Можно отменить действиеДаНет
Доступен новый IDНет (ещё не создан)Да
Типичное применениеВалидация, нормализация, запретУведомления, интеграции, задачи

Практический разбор: если вам нужно «проверить и не пустить» — берите before. Если «отреагировать на свершившийся факт и узнать ID» — берите after. И держите в голове опасную комбинацию: тяжёлую интеграцию нельзя вешать на before (замедлит и может сорвать само действие), а валидацию — на after (данные уже записаны, отменять поздно).

Ось действия: before → запись → after before-событие данные ещё не записаны можно менять и отменить Запись в базу действие свершилось after-событие данные записаны, есть ID отменить нельзя Валидация, нормализация, запрет действия Уведомления, интеграции, постановка задач
Схема: before-событие идёт до записи — можно менять данные и отменить действие; after-событие идёт после записи — доступен ID, но отменить уже нельзя.

События модулей: главные точки расширения

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

МодульСобытиеКогда вызывается
mainOnBeforeUserAdd / OnAfterUserAddДо/после регистрации пользователя
mainOnBeforeUserUpdate / OnAfterUserUpdateДо/после изменения профиля
mainOnAfterUserAuthorizeПосле успешной авторизации
iblockOnBeforeIBlockElementAdd / OnAfterIBlockElementAddДо/после создания элемента (товара)
iblockOnBeforeIBlockElementUpdate / OnAfterIBlockElementUpdateДо/после обновления элемента
iblockOnAfterIBlockElementDeleteПосле удаления элемента
saleOnSaleOrderBeforeSaved / OnSaleOrderSavedДо/после сохранения заказа
saleOnSaleStatusOrderChangeПри смене статуса заказа
saleOnSaleOrderPaidПри переводе заказа в «оплачен»
catalogOnPriceUpdate / OnPriceAddПри изменении/добавлении цены
mainOnEndBufferContentПеред отдачей готового HTML (постобработка)

Это не полный список, а «рабочий минимум», который закрывает большинство задач магазина. Точные имена и параметры конкретного события всегда сверяйте с официальной документацией — там указано, что событие передаёт и как отменяется. Отдельно обратите внимание: у модуля sale есть и старые события, и D7-события (в его новой ветке), поэтому перед подпиской важно понять, какое поколение вы используете.

События инфоблоков и каталога

Для магазина инфоблоки — это товары, а значит события инфоблоков используются постоянно. Самое ходовое — OnBeforeIBlockElementUpdate и его пара OnAfterIBlockElementUpdate. Первое даёт вмешаться в сохранение товара (проверить, нормализовать, запретить), второе — отреагировать на уже сохранённый (пересчитать, синхронизировать).

Разберём типичную задачу: перед сохранением товара нужно гарантировать, что артикул записан в едином формате (верхний регистр, без лишних пробелов), а название непустое. Это чистый случай для before-события:

<?php
namespace App\Handlers;

class CatalogHandlers
{
    public static function onBeforeElementUpdate(&$arFields)
    {
        // Нормализуем название
        if (isset($arFields['NAME'])) {
            $arFields['NAME'] = trim($arFields['NAME']);
            if ($arFields['NAME'] === '') {
                global $APPLICATION;
                $APPLICATION->throwException('Название товара не может быть пустым');
                return false; // отменяем обновление
            }
        }
        return true;
    }
}

А теперь after-сценарий: товар обновили, и нужно пометить его раздел как «требует пересчёта остатков» или отправить сигнал во внешнюю систему. Тут доступен реальный ID, поэтому берём OnAfterIBlockElementUpdate:

<?php
public static function onAfterElementUpdate(&$arFields)
{
    $elementId = (int)($arFields['ID'] ?? 0);
    if ($elementId <= 0) {
        return;
    }
    // Ставим задачу на пересчёт/выгрузку, а не делаем её прямо здесь
    \App\Queue\Tasks::push('product_changed', ['id' => $elementId]);
}

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

События цен (catalog: OnPriceUpdate, OnPriceAdd) полезны, когда логика завязана именно на цену, а не на карточку товара, — например, при изменении базовой цены пересчитать зависимые типы цен или сбросить связанный кэш. А как события каталога связаны с обменом товаров и цен с учётной системой, подробно разбираем в материале про интеграцию магазина с 1С.

События заказа и корзины (модуль sale)

Заказ — сердце магазина, и именно вокруг него крутится больше всего кастомной логики. Модуль sale даёт богатый набор событий: на сохранение заказа, смену статуса, оплату, изменение корзины. С их помощью реализуют автостатусы, уведомления, списание бонусов, интеграцию с CRM и складом.

Ключевые события заказа:

Пример D7-обработчика на сохранение заказа, который дописывает служебный комментарий для самовывоза:

<?php
use Bitrix\Main\Event;

class OrderHandlers
{
    public static function onOrderSaved(Event $event)
    {
        /** @var \Bitrix\Sale\Order $order */
        $order = $event->getParameter('ENTITY');
        $isNew = $event->getParameter('IS_NEW');
        if (!$isNew) {
            return; // реагируем только на первое сохранение
        }

        $orderId = $order->getId();
        // ... прочитать способ доставки, при самовывозе пометить для склада ...
        \App\Queue\Tasks::push('order_created', ['id' => $orderId]);
    }
}

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

Про D7-ветку sale: в актуальных версиях модуль sale живёт на D7-объектах (\Bitrix\Sale\Order, \Bitrix\Sale\Basket). Поэтому и события у него D7-стиля: обработчик получает объект Event, а сущность достаёт через getParameter('ENTITY'). Старые процедурные события заказа тоже существуют для совместимости, но новый код логичнее писать на D7-событиях.

События ORM: onBeforeAdd и onAfterUpdate

Отдельное большое семейство — события ORM. Они относятся не к модулям, а к сущностям DataManager (highload-блоки, свои таблицы, D7-сущности). Как только вы работаете с данными через ORM, у их таблиц автоматически появляются события жизненного цикла: onBeforeAdd, onAfterAdd, onBeforeUpdate, onAfterUpdate, onBeforeDelete, onAfterDelete.

Есть два способа их использовать. Первый — объявить обработчик прямо в классе сущности (метод-обработчик события), что удобно, когда логика неразрывна с самой таблицей. Второй — подписаться извне через EventManager, указав полное имя класса сущности как «модуль». Пример обработчика в самом классе:

<?php
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Event;
use Bitrix\Main\ORM\EventResult;

class StatusLogTable extends DataManager
{
    public static function onBeforeAdd(Event $event)
    {
        $result = new EventResult();
        $data = $event->getParameter('fields');

        // Проставим дату, если её не передали
        if (empty($data['CREATED_AT'])) {
            $result->modifyFields(['CREATED_AT' => new \Bitrix\Main\Type\DateTime()]);
        }
        return $result; // modifyFields изменит сохраняемые поля
    }

    public static function onAfterUpdate(Event $event)
    {
        // Реакция на уже применённое обновление записи
    }
}

Ключевые отличия событий ORM от старых модульных: обработчик всегда получает объект Event (ORM-версию), данные достаёт через getParameter(), а изменить сохраняемые поля можно только «правильно» — через EventResult::modifyFields(), а не прямой правкой массива по ссылке. Отмену действия тоже оформляют через EventResult с ошибкой. Это строже и предсказуемее старого API. Если вы строите бизнес-логику на собственных D7-сущностях, события ORM — основной инструмент; их устройство мы детально разбираем в статье про D7 ORM в 1С-Битрикс.

Отмена действия и исключения

Одна из самых востребованных возможностей — запретить действие из обработчика. «Не давать создать заказ без телефона», «не сохранять товар с дублирующимся артикулом», «не регистрировать пользователя с запрещённого домена» — всё это отмены в before-событиях. Но механика отмены различается по поколениям событий, и здесь легко ошибиться.

<?php
// Отмена в старом before-событии
public static function onBeforeUserAdd(&$arFields)
{
    $email = $arFields['EMAIL'] ?? '';
    if (str_ends_with($email, '@example-blocked.tld')) {
        global $APPLICATION;
        $APPLICATION->throwException('Регистрация с этого домена запрещена');
        // Пишем причину в лог — иначе отладка вслепую
        \App\Log::warning('user.add.blocked', ['email' => $email]);
        return false;
    }
    return true;
}

Два правила, которые уберегут от боли. Первое: отменять можно только в before. After-событие вызывается после того, как всё уже записано; вернуть там false бесполезно — данные не откатятся. Если нужна отмена, ищите before-пару события. Второе: всегда логируйте причину отмены. «Заказ не сохраняется, а почему — непонятно» — типичная жалоба, за которой стоит молчаливый обработчик, отменяющий действие без единой записи в лог. Сообщение пользователю плюс запись в лог — обязательный минимум.

before-обработчик решает: пустить или отменить Проверка данные валидны? Да → return true действие продолжается Нет → return false throwException + лог действие отменено Только before after отменить нельзя
Схема: before-обработчик проверяет данные — при успехе действие продолжается, при провале возвращает false с сообщением и записью в лог, и действие отменяется; в after отмена невозможна.

Порядок и приоритет обработчиков

На одно событие можно подписать сколько угодно обработчиков — из вашего кода, из ядра, из сторонних модулей. Все они выполняются последовательно, и порядок задаётся сортировкой. У AddEventHandler это четвёртый аргумент (по умолчанию 100), у D7-го addEventHandler — параметр приоритета с той же логикой: чем меньше число, тем раньше выполнится обработчик.

<?php
$em = \Bitrix\Main\EventManager::getInstance();

// Выполнится раньше (сортировка 50)
$em->addEventHandler('iblock', 'OnBeforeIBlockElementUpdate',
    ['\App\Handlers\CatalogHandlers', 'normalize'], false, 50);

// Выполнится позже (сортировка 200)
$em->addEventHandler('iblock', 'OnBeforeIBlockElementUpdate',
    ['\App\Handlers\CatalogHandlers', 'validate'], false, 200);

Для большинства задач порядок неважен: обработчики независимы, и в каком очерёдности они отработают — всё равно. Но есть случаи, где сортировка критична:

Главный совет: не полагайтесь на «естественный» порядок регистрации. Он кажется предсказуемым, пока в проект не добавят ещё один модуль или не поменяют порядок строк в init.php. Если очерёдность важна — задавайте сортировку явными числами и оставляйте между ними зазор (50, 100, 150), чтобы позже было куда вставить промежуточный обработчик.

Обработчики выполняются по сортировке (меньше = раньше) Событие OnBefore…Update sort 50 normalize() sort 100 validate() sort 200 notifyOrPush() Оставляйте зазор между числами (50/100/150) — будет куда вставить новый обработчик
Схема: одно событие запускает цепочку обработчиков по возрастанию сортировки; нормализатор идёт раньше валидатора, а тяжёлые реакции — в конце.

Пример: модификация заказа перед сохранением

Соберём первый полноценный кейс. Задача: перед сохранением заказа нужно автоматически проставить служебное свойство — например, канал оформления («сайт», «мобильное приложение») и пометку о необходимости звонка, если сумма выше порога. Это классический before-сценарий: данные ещё не записаны, и мы влияем на итог.

Регистрация в init.php:

<?php
$em = \Bitrix\Main\EventManager::getInstance();
$em->addEventHandler(
    'sale',
    'OnSaleOrderBeforeSaved',
    ['\App\Handlers\OrderHandlers', 'onBeforeOrderSaved']
);

Тело обработчика в классе:

<?php
namespace App\Handlers;

use Bitrix\Main\Event;

class OrderHandlers
{
    public static function onBeforeOrderSaved(Event $event)
    {
        /** @var \Bitrix\Sale\Order $order */
        $order = $event->getParameter('ENTITY');

        // Помечаем крупные заказы для обязательного звонка
        $price = (float)$order->getPrice();
        if ($price >= 50000) {
            $propertyCollection = $order->getPropertyCollection();
            $callProp = $propertyCollection->getItemByOrderPropertyCode('NEED_CALL');
            if ($callProp) {
                $callProp->setValue('Y');
            }
        }
        // Возврат управления — заказ сохранится с нашими правками
    }
}

Разберём важные детали. Мы работаем с D7-объектом Order, а не с массивом полей: читаем сумму методом getPrice(), свойства — через коллекцию свойств. Мы не вызываем save() внутри обработчика — заказ и так сохраняется ядром, а наша задача лишь подготовить его состояние. Вызов save() здесь спровоцировал бы повторное сохранение и, возможно, рекурсию событий — типичная ошибка. В before-обработчике меняем состояние объекта и отдаём управление, а запись сделает платформа.

Пример: автостатусы заказа

Второй кейс — автоматические статусы. Задача: когда заказ переходит в статус «Оплачен», нужно автоматически перевести его в «Готов к сборке» и запустить резервирование. Отлично ложится на событие смены статуса OnSaleStatusOrderChange (или, в D7-ветке, на событие оплаты).

<?php
// Регистрация (старое событие смены статуса)
AddEventHandler('sale', 'OnSaleStatusOrderChange',
    ['\App\Handlers\OrderHandlers', 'onStatusChange']);

// Обработчик
public static function onStatusChange($orderId, $statusId)
{
    // Реагируем только на переход в «Оплачен» (условно статус 'P')
    if ($statusId !== 'P') {
        return;
    }
    // Ставим задачу: перевести в сборку и зарезервировать
    \App\Queue\Tasks::push('order_paid', ['id' => (int)$orderId]);
}

Здесь спрятаны две важные мысли. Первая: не меняйте статус заказа прямо внутри обработчика смены статуса «в лоб» — это верный путь к рекурсии, когда ваше изменение статуса снова поднимает то же событие, которое снова меняет статус, и так по кругу. Безопаснее зафиксировать намерение (поставить задачу) и выполнить смену отдельным управляемым шагом, а если делаете это синхронно — обязательно предусмотрите защиту от повторного входа (флаг «уже обрабатывается»).

Вторая мысль: автостатусы легко превращаются в запутанный «конечный автомат», где статусы переключают друг друга по цепочке. Держите правила переходов в одном месте и прозрачными — иначе через полгода никто не сможет ответить, почему заказ сам перескочил из одного статуса в другой. Событие — это триггер, а логику переходов лучше вынести в отдельный сервис-класс, который легко прочитать и протестировать.

Пример: уведомления по событию

Третий кейс — уведомления. Задача: после создания нового заказа отправить менеджеру письмо, а клиенту — сообщение в мессенджер. Это after-сценарий (OnSaleOrderSaved с проверкой IS_NEW), потому что нам нужен готовый заказ с ID и суммой, и отменять ничего не надо.

<?php
public static function onOrderSaved(\Bitrix\Main\Event $event)
{
    /** @var \Bitrix\Sale\Order $order */
    $order = $event->getParameter('ENTITY');
    if (!$event->getParameter('IS_NEW')) {
        return; // только новые заказы
    }

    // НЕ отправляем письмо прямо здесь — ставим в очередь
    \App\Queue\Tasks::push('notify_new_order', [
        'id'    => $order->getId(),
        'sum'   => $order->getPrice(),
    ]);
}

Ключевая рекомендация выделена намеренно: не отправляйте уведомления синхронно внутри обработчика. Отправка письма или запрос к API мессенджера — это обращение к внешней сети, которое может длиться секунды или упасть по таймауту. Если делать это прямо в обработчике сохранения заказа, то, во-первых, оформление заказа для клиента затянется на время отправки, а во-вторых, падение почтового сервера может сорвать сам заказ (или бросить ошибку в неподходящий момент).

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

Идемпотентность. Событие может сработать не ровно один раз: заказ пересохранили, обмен прогнал его повторно, задача в очереди выполнилась дважды из-за сбоя. Проектируйте реакции так, чтобы повтор не навредил: помечайте «уведомление по заказу №N уже отправлено», проверяйте флаг перед действием. Иначе клиент получит три одинаковых письма, а бонусы начислятся дважды.

Пример: пересчёт и интеграция по событию

Четвёртый кейс — интеграция. Задача: при изменении товара (цена, остаток, свойства) отправить его в внешнюю систему — CRM, маркетплейс, поисковый индекс. Триггер — OnAfterIBlockElementUpdate и OnAfterIBlockElementAdd. И именно здесь производительность становится критичной.

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

<?php
public static function onAfterElementUpdate(&$arFields)
{
    $id = (int)($arFields['ID'] ?? 0);
    if ($id <= 0) return;

    // Только пометка «товар изменён» — быстрая запись в свою таблицу
    \App\Sync\ChangeLog::mark($id, 'update');
}

Обработчик делает единственную дешёвую операцию — помечает товар в журнале изменений (своя таблица или очередь). А отдельный агент раз в N минут забирает накопленные метки, группирует их и выгружает пачкой в один-два запроса к внешней системе. Так событие остаётся мгновенным, обмен с 1С не тормозит, а внешняя система получает изменения с небольшой предсказуемой задержкой — что почти всегда приемлемо.

Эта развязка «событие → журнал → фоновая выгрузка» — фундамент любой надёжной интеграции на событиях. Она делает систему устойчивой к сбоям (упал внешний API — метки просто накопятся и выгрузятся позже) и к нагрузке (тысяча изменений превращается в одну пачку, а не тысячу запросов). Как выстроить такую двустороннюю синхронизацию с учётной системой, подробно разбираем в материале про интеграцию магазина с 1С.

Отложенные задачи из обработчика

Мы несколько раз повторили: тяжёлое из обработчика надо откладывать. Разберём, как именно это делают в Битриксе. Есть три основных механизма, и выбор зависит от требований к задержке и надёжности.

МеханизмКак работаетКогда уместен
АгентШтатные агенты Битрикса: функция выполняется по расписанию (на хите или по крону)Периодические пачечные задачи: выгрузки, пересчёты
Своя очередьОбработчик пишет задачу в таблицу, отдельный процесс её разбираетКогда нужен контроль порядка, повторов и статусов
Внешняя очередьПостановка в брокер сообщений, воркер обрабатывает асинхронноВысокая нагрузка, изоляция, масштабирование воркеров

Для большинства магазинов достаточно первых двух. Простейший вариант — агент: обработчик лишь ставит отметку, а агент, запускаемый по крону, раз в несколько минут разбирает накопленное. Важно, чтобы агенты запускались именно кроном, а не «на хите пользователя», — иначе тяжёлый агент отработает во время чьего-то запроса и замедлит страницу ровно тому посетителю, которому не повезло его запустить.

<?php
// Агент разбирает накопленные изменения пачкой
class SyncAgent
{
    public static function run()
    {
        $batch = \App\Sync\ChangeLog::takeBatch(200); // до 200 за раз
        if ($batch) {
            \App\Sync\Exporter::export($batch);        // один запрос на пачку
            \App\Sync\ChangeLog::markDone($batch);
        }
        return '\App\SyncAgent::run();'; // самоперезапуск агента
    }
}

Смысл паттерна в развязке во времени: событие происходит мгновенно и лишь фиксирует «надо сделать», а собственно работа выполняется позже, отдельно, пачками и с контролем повторов. Это делает и само действие (сохранение заказа, обновление товара) быстрым, и тяжёлую работу — управляемой. Если у вас уже есть очередь или брокер, обработчик просто кладёт задачу туда; принцип тот же.

Производительность: тяжёлый обработчик тормозит

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

Наглядный пример. Вы повесили на OnAfterIBlockElementUpdate обработчик, который на каждое обновление товара делает запрос к внешнему API длительностью 300 мс. В админке правка одного товара стала медленнее на треть секунды — терпимо. Но пришёл обмен с 1С и обновил 5000 товаров: 5000 × 300 мс = 25 минут только на ваши запросы, обмен упёрся в таймаут и упал. Один невинный обработчик обрушил интеграцию.

Правила, которые держат обработчики быстрыми:

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

Отладка: почему событие не сработало

«Написал обработчик, а он не срабатывает» — самая частая жалоба новичка. Причин у молчащего обработчика немного, и они хорошо диагностируются по чек-листу. Пройдём по порядку от самого частого к более редкому.

  1. Не тот init.php. Проверьте, что регистрация в том файле, который реально подключается. Если проект работает через /local/, а вы дописали в /bitrix/php_interface/init.php, — код может не выполняться. Поставьте в начало нужного init.php временную запись в лог и убедитесь, что файл подключается.
  2. Опечатка в модуле или имени события. sale — не Sale, OnAfterIBlockElementAdd — не OnAfterIblockElementAdd. Регистр и точное имя важны. Сверьтесь с документацией посимвольно.
  3. Не то поколение события. Подписались на D7-событие старым способом или наоборот. Проверьте, как событие вызывается в ядре, и подпишитесь соответствующим API.
  4. Обработчик падает раньше логики. Ошибка в классе, неверный namespace, класс не найден автозагрузкой. Включите отображение и логирование ошибок, посмотрите лог PHP.
  5. Действие идёт в обход события. Прямой SQL, низкоуровневый импорт или метод, который намеренно не поднимает события. Тогда обработчик и не должен срабатывать — ищите другую точку.

Универсальный приём отладки — лог в самой первой строке обработчика. Прежде чем разбираться, что обработчик делает не так, убедитесь, что управление вообще в него входит:

<?php
public static function onOrderSaved(\Bitrix\Main\Event $event)
{
    // Первым делом фиксируем сам факт входа
    \App\Log::debug('order.saved.handler.enter', [
        'orderId' => $event->getParameter('ENTITY')->getId(),
    ]);
    // ... остальная логика ...
}

Если записи в логе нет — проблема в регистрации (пункты 1–3): обработчик не вызывается вовсе. Если запись есть, а результата нет — проблема внутри логики (пункт 4 или бизнес-условия). Это простое разделение «входит / не входит» экономит часы: вы сразу знаете, где искать. Для событий-отмен добавляйте в лог и причину отказа — иначе «действие не выполняется, а почему — молчок» превращается в мучительный поиск вслепую.

Инструмент под рукой: держите тонкую обёртку-логер (как \App\Log в примерах) с записью в файл или в журнал событий Битрикса. Единая точка логирования обработчиков бесценна: включили подробный уровень — видите весь поток событий; выключили — тишина в продакшене. Это дешевле, чем каждый раз расставлять var_dump по коду.

Регистрация обработчиков из своего модуля

Когда кастомной логики становится много, регистрировать всё в init.php перестаёт быть удобным. Зрелый путь — собственный модуль в /local/modules/, который сам регистрирует свои обработчики при установке. Это делает логику самодостаточной: подключили модуль — подписки появились, отключили — исчезли, без ручной правки init.php.

У модуля есть класс установки с методами InstallEvents() / UnInstallEvents(), где через registerEventHandler() / unRegisterEventHandler() подписки сохраняются в базу. В отличие от регистрации в init.php (она действует только на время хита), такие подписки становятся постоянными и переживают перезагрузку.

<?php
// В классе установки модуля
public function InstallEvents()
{
    $em = \Bitrix\Main\EventManager::getInstance();
    $em->registerEventHandler(
        'sale', 'OnSaleOrderSaved',
        $this->MODULE_ID,               // ваш модуль
        '\App\Handlers\OrderHandlers', 'onOrderSaved'
    );
}

public function UnInstallEvents()
{
    $em = \Bitrix\Main\EventManager::getInstance();
    $em->unRegisterEventHandler(
        'sale', 'OnSaleOrderSaved',
        $this->MODULE_ID,
        '\App\Handlers\OrderHandlers', 'onOrderSaved'
    );
}

Разница между addEventHandler и registerEventHandler принципиальна. addEventHandler (и старый AddEventHandler) действует только в рамках текущего хита — поэтому его и зовут в init.php, который выполняется каждый раз. registerEventHandler сохраняет подписку в базу навсегда, поэтому его вызывают один раз при установке модуля. Не смешивайте их: подписка из модуля не должна дублироваться в init.php, иначе обработчик выполнится дважды.

Когда что выбирать? Для небольших проектов и точечной логики достаточно init.php с addEventHandler — просто и наглядно. Для крупного проекта с переиспользуемой логикой, которую хочется включать/выключать как единое целое и переносить между сайтами, оправдан свой модуль с постоянной регистрацией. Оба подхода легитимны; выбор — вопрос масштаба и повторного использования.

Частые ошибки

Ошибки с обработчиками повторяются из проекта в проект. Зная их заранее, вы сэкономите дни отладки и убережёте магазин от коварных багов, которые проявляются только под нагрузкой.

Чек-лист по обработчикам

Короткий список, по которому удобно свериться при разработке и перед релизом магазина на Битриксе:

Вывод

Механизм событий — это то, что превращает 1С-Битрикс из «коробки, которую страшно трогать» в расширяемую платформу. Вместо хирургии по чужому коду вы подписываете свои обработчики на нужные моменты — до и после регистрации пользователя, обновления товара, сохранения заказа, смены статуса — и меняете поведение системы, не тронув ни строчки ядра. Регистрация живёт в init.php, тело — в аккуратных классах, а два API (AddEventHandler и D7-й addEventHandler) покрывают и старые модульные события, и события ORM.

Практический стержень зрелой работы с событиями — дисциплина. Запреты — только в before и с логом; реакции и интеграции — в after и через отложенные задачи, чтобы не тормозить действие и не рушить обмен с 1С; порядок обработчиков задан явной сортировкой, где он важен; реакции идемпотентны и защищены от рекурсии; каждый обработчик начинается с точки логирования. Соблюдая эти правила, вы получаете гибкую бизнес-логику, которая переживает обновления платформы, не проседает под нагрузкой и понятна следующему разработчику. Если нужна такая архитектура под ваш проект, мы выстраиваем её в рамках highload-проектов на Битриксе и корпоративных сайтов на Битриксе. Другие материалы по архитектуре и производительности собраны в рубрике «Разработка и архитектура».

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

Что такое событие в 1С-Битрикс простыми словами?

Событие — это точка в коде платформы, в которой ядро «останавливается и спрашивает»: не хочет ли кто-нибудь вмешаться? Например, перед сохранением заказа модуль торгового каталога вызывает событие OnSaleOrderBeforeSaved, перед обновлением элемента инфоблока — OnBeforeIBlockElementUpdate. Если вы заранее подписали на это событие свою функцию (обработчик), она выполнится в этот момент и сможет прочитать или изменить данные, а иногда и отменить само действие. Это штатный механизм расширения: вы меняете поведение системы, не трогая её исходный код.

Чем AddEventHandler отличается от EventManager и addEventHandler D7?

Это два поколения одного механизма. AddEventHandler — процедурная функция из старого API (D5/D6): вы указываете модуль, имя события и вашу функцию. EventManager с методом addEventHandler — объектный интерфейс из D7, он делает то же самое, но в стиле пространств имён и классов. Для событий, которые вызываются через ExecuteModuleEventEx (большинство «модульных» событий вроде OnBeforeUserAdd), оба способа взаимозаменяемы, и обработчик, зарегистрированный любым из них, отработает. Разница в стиле и в том, что часть новых событий (например, событий ORM и Sale D7) публикуется только через новый механизм send/addEventHandler.

Где правильно регистрировать обработчики событий?

Штатное место — файл init.php. В современных проектах это /local/php_interface/init.php, в старых — /bitrix/php_interface/init.php. Этот файл платформа подключает на каждом хите после инициализации ядра, поэтому регистрация обработчиков в нём гарантированно происходит до того, как события начнут вызываться. Правило простое: сама регистрация (AddEventHandler / addEventHandler) — в init.php, а тело обработчика — в отдельном классе, который подключается автозагрузкой. Так init.php не превращается в свалку из сотен строк логики.

Как из обработчика отменить действие — например, запретить сохранение заказа?

Способ зависит от типа события. В «старых» before-событиях модулей обработчик возвращает false, и платформа прерывает действие (так работает, например, OnBeforeUserAdd). В событиях D7 и ORM отмену делают через исключение: обработчик выбрасывает \Bitrix\Main\ORM\EventResult с ошибкой или бросает исключение, которое ядро перехватывает и отменяет операцию. Важно понимать, что отменять можно только в before-событиях: after-событие вызывается уже после того, как действие совершилось, и «откатить» его нельзя. Перед отменой всегда записывайте причину в лог, иначе отладка превращается в гадание.

Почему мой обработчик не срабатывает?

Пять самых частых причин. Первая: обработчик зарегистрирован не в том init.php (например, в старом bitrix/php_interface, а проект работает через local). Вторая: опечатка в имени модуля или события — регистр важен, sale это не catalog. Третья: событие вызывается через другой механизм, чем вы подписались (старое before-событие вместо D7-события или наоборот). Четвёртая: ваш обработчик падает с ошибкой раньше, чем доходит до логики, а ошибки скрыты. Пятая: действие идёт в обход события — например, прямой SQL-запрос или импорт, который не поднимает событий ORM. Диагностика всегда одна: включите лог в самом начале обработчика и проверьте, входит ли управление внутрь.

Обработчик тормозит сайт — что делать?

Обработчик выполняется синхронно внутри действия, поэтому тяжёлая работа в нём напрямую замедляет то, к чему он подписан. Если в OnAfterIBlockElementUpdate вы дёргаете внешний API, а он отвечает три секунды, — сохранение каждого товара будет длиться на три секунды дольше, и админ это почувствует, а массовый импорт встанет колом. Правило: в обработчике делайте только быстрое и необходимое, а всё тяжёлое (отправку писем, обращения к внешним системам, пересчёты по каталогу) откладывайте — ставьте в очередь, создавайте агент или фоновую задачу, а само обращение к API выполняйте отдельным процессом. Обработчик должен лишь зафиксировать «надо сделать», а не делать это прямо здесь.

Можно ли повесить несколько обработчиков на одно событие и в каком порядке они выполнятся?

Да, на одно событие можно подписать сколько угодно обработчиков — из разных модулей, из ядра и из вашего кода. Выполняются они последовательно, а порядок задаётся сортировкой: у AddEventHandler это числовой параметр сортировки (по умолчанию 100), у addEventHandler D7 — аналогичный параметр приоритета. Чем меньше число, тем раньше выполнится обработчик. Для большинства задач порядок неважен, но он критичен, когда один обработчик готовит данные для другого или когда before-обработчик может отменить действие: если он вернул false или бросил исключение, следующие в некоторых сценариях уже не выполнятся. Не полагайтесь на «естественный» порядок регистрации — задавайте сортировку явно, если она важна.

Чем события отличаются от прямой правки компонента или ядра?

Правка ядра (файлов в /bitrix/modules/) теряется при первом же обновлении платформы и по сути запрещена. Правка компонента решает задачи вывода, но не помогает вмешаться в бизнес-логику сохранения данных. События — это официально поддерживаемая точка расширения: платформа сама зовёт ваш код в нужный момент, и этот контракт не меняется при обновлениях. Поэтому логику «при создании заказа сделай X», «перед обновлением товара проверь Y», «после регистрации пользователя отправь Z» реализуют именно обработчиками событий, а не хирургией по чужому коду. Это делает проект обновляемым и предсказуемым.

Поделиться:

Нужна расширяемая логика на 1С-Битрикс?

Реализуем бизнес-логику на обработчиках событий без правки ядра: автостатусы, уведомления, интеграции и пересчёты через отложенные задачи. Аккуратно, обновляемо и без просадки скорости. Рассчитаем работу по вашему проекту.

Услуга «Highload-проект»

Игорь Воскресенский

Команда студии B2Bsite. С 2014 года разрабатываем, ускоряем и поддерживаем интернет-магазины на 1С-Битрикс: бизнес-логика на обработчиках событий без правки ядра, D7 ORM, интеграции и обмен с 1С, кэш и производительность для среднего и крупного бизнеса.

← Все статьи блога