Главная/Статьи/События в Битрикс — подписки, свои события, отладка

События в Битрикс — подписки, свои события, отладка

Разбираем систему событий: OnBeforeIBlockElementAdd, свои события, EventManager D7. Как не сломать сайт обработчиком и как дебажить.

ДМ
Дмитрий Мещеряков
📅 19 августа 2026 г.📖 8 мин чтения

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

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

Как работают события

Событийная модель — основа расширяемости Битрикс. Ядро и модули генерируют события в ключевых точках, а ваш код может на них подписаться.

text
Действие пользователя

Ядро Битрикс вызывает OnBefore*

Ваш обработчик (можно отменить/изменить)

Ядро выполняет действие

Ядро вызывает OnAfter*

Ваш обработчик (логирование, уведомления)
💡 Совет

OnBefore* — можно отменить операцию или изменить данные. OnAfter* — операция уже выполнена, только реагируем.

Подписка на события

Через init.php (старый способ)

php
<?php
// /bitrix/php_interface/init.php

// Функция-обработчик
function OnBeforeIBlockElementAddHandler(&$arFields)
{
    // Автозаполнение кода из названия
    if (empty($arFields['CODE']) && !empty($arFields['NAME'])) {
        $arFields['CODE'] = \CUtil::translit(
            $arFields['NAME'],
            'ru',
            ['replace_space' => '-', 'replace_other' => '-']
        );
    }
}

// Регистрация
AddEventHandler(
    'iblock',                              // Модуль
    'OnBeforeIBlockElementAdd',            // Событие
    'OnBeforeIBlockElementAddHandler'      // Функция
);
💡 Совет

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

Отсюда классические грабли: registerEventHandler() в init.php добавляет новую запись при каждом хите, и через день обработчик выполняется тысячи раз за запрос. Из init.php — только addEventHandler().

Через EventManager D7 (рекомендуется)

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

$eventManager = EventManager::getInstance();

// Подписка с анонимной функцией
$eventManager->addEventHandler(
    'iblock',
    'OnBeforeIBlockElementAdd',
    function (&$arFields) {
        if ($arFields['IBLOCK_ID'] == 2) {
            $arFields['ACTIVE'] = 'N'; // Новые товары неактивны
        }
    }
);

// Подписка на класс (рекомендуется для сложной логики)
$eventManager->addEventHandler(
    'iblock',
    'OnAfterIBlockElementAdd',
    ['\\Local\\EventHandlers\\IblockHandler', 'onAfterElementAdd']
);
⚠️ Важно

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

Класс-обработчик

php
<?php
// /local/lib/EventHandlers/IblockHandler.php
namespace Local\EventHandlers;

use Bitrix\Main\Loader;

class IblockHandler
{
    private const CATALOG_IBLOCK_ID = 2;

    public static function onAfterElementAdd(array &$arFields): void
    {
        if ($arFields['IBLOCK_ID'] != self::CATALOG_IBLOCK_ID) {
            return;
        }

        // Логируем добавление товара
        self::logAction('add', $arFields['ID'], $arFields);

        // Сбрасываем кэш каталога
        self::clearCatalogCache();

        // Уведомляем администратора
        if ($arFields['ACTIVE'] === 'Y') {
            self::notifyAdmin($arFields);
        }
    }

    public static function onBeforeElementUpdate(array &$arFields): bool
    {
        // Запрещаем менять код у опубликованных товаров
        if (isset($arFields['CODE'])) {
            Loader::includeModule('iblock');
            $element = \CIBlockElement::GetByID($arFields['ID'])->Fetch();
            if ($element && $element['ACTIVE'] === 'Y' && $element['CODE'] !== $arFields['CODE']) {
                global $APPLICATION;
                $APPLICATION->ThrowException('Нельзя менять код опубликованного товара');
                return false;
            }
        }
        return true;
    }
    private static function logAction(string $action, int $elementId, array $fields): void
    {
        // Логика логирования
    }
    private static function clearCatalogCache(): void
    {
        $taggedCache = \Bitrix\Main\Application::getInstance()->getTaggedCache();
        $taggedCache->clearByTag('iblock_id_' . self::CATALOG_IBLOCK_ID);
    }
    private static function notifyAdmin(array $fields): void
    {
        // Отправка уведомления
    }
}
💡 Совет

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

Популярные события

Инфоблоки

php
<?php
use Bitrix\Main\EventManager;
$em = EventManager::getInstance();
// Элементы инфоблока
$em->addEventHandler('iblock', 'OnBeforeIBlockElementAdd', $handler);
$em->addEventHandler('iblock', 'OnAfterIBlockElementAdd', $handler);
$em->addEventHandler('iblock', 'OnBeforeIBlockElementUpdate', $handler);
$em->addEventHandler('iblock', 'OnAfterIBlockElementUpdate', $handler);
$em->addEventHandler('iblock', 'OnBeforeIBlockElementDelete', $handler);
$em->addEventHandler('iblock', 'OnAfterIBlockElementDelete', $handler);
// Разделы
$em->addEventHandler('iblock', 'OnBeforeIBlockSectionAdd', $handler);
$em->addEventHandler('iblock', 'OnAfterIBlockSectionAdd', $handler);

Пользователи

php
<?php
// Регистрация
$em->addEventHandler('main', 'OnBeforeUserRegister', $handler);
$em->addEventHandler('main', 'OnAfterUserRegister', $handler);
// Авторизация
$em->addEventHandler('main', 'OnBeforeUserLogin', $handler);
$em->addEventHandler('main', 'OnAfterUserLogin', $handler);
$em->addEventHandler('main', 'OnAfterUserLogout', $handler);
// Изменение
$em->addEventHandler('main', 'OnBeforeUserUpdate', $handler);
$em->addEventHandler('main', 'OnAfterUserUpdate', $handler);

Заказы (sale)

php
<?php
// Создание заказа
$em->addEventHandler('sale', 'OnSaleOrderBeforeSaved', $handler);
$em->addEventHandler('sale', 'OnSaleOrderSaved', $handler);
// Смена статуса
$em->addEventHandler('sale', 'OnSaleStatusOrder', $handler);
// Оплата
$em->addEventHandler('sale', 'OnSalePayOrder', $handler);
// Отмена
$em->addEventHandler('sale', 'OnSaleCancelOrder', $handler);

Создание своих событий

Генерация события

php
<?php
namespace Local\Service;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
class OrderService
{
    public function processOrder(int $orderId): void
    {
        // Бизнес-логика...
        // Генерируем событие
        $event = new Event(
            'local',                    // Модуль
            'OnOrderProcessed',         // Имя события
            [
                'ORDER_ID' => $orderId,
                'STATUS' => 'processed',
                'TIMESTAMP' => time(),
            ]
        );
        $event->send();
        // Проверяем результаты обработчиков
        foreach ($event->getResults() as $result) {
            if ($result->getType() === EventResult::ERROR) {
                throw new \RuntimeException('Обработчик вернул ошибку');
            }
        }
    }
}

Подписка на своё событие

php
<?php
use Bitrix\Main\EventManager;

$eventManager = EventManager::getInstance();
$eventManager->addEventHandler(
    'local',
    'OnOrderProcessed',
    function (\Bitrix\Main\Event $event) {
        $params = $event->getParameters();
        // Отправляем в CRM
        CrmIntegration::sendOrder($params['ORDER_ID']);
        // Логируем
        Logger::info('Order processed', $params);
    }
);

Событие с возможностью отмены

php
<?php
namespace Local\Service;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
class PaymentService
{
    public function processPayment(int $paymentId, float $amount): bool
    {
        // Событие ДО обработки
        $event = new Event('local', 'OnBeforePaymentProcess', [
            'PAYMENT_ID' => $paymentId,
            'AMOUNT' => $amount,
        ]);
        $event->send();
        // Проверяем, не отменил ли кто-то операцию
        foreach ($event->getResults() as $result) {
            if ($result->getType() === EventResult::ERROR) {
                return false;
            }
        }
        // Выполняем оплату...
        $success = $this->doPayment($paymentId, $amount);
        // Событие ПОСЛЕ обработки
        if ($success) {
            $afterEvent = new Event('local', 'OnAfterPaymentProcess', [
                'PAYMENT_ID' => $paymentId,
                'AMOUNT' => $amount,
                'SUCCESS' => true,
            ]);
            $afterEvent->send();
        }
        return $success;
    }
}
// Обработчик с отменой
$eventManager->addEventHandler(
    'local',
    'OnBeforePaymentProcess',
    function (Event $event) {
        $params = $event->getParameters();
        // Проверяем лимит
        if ($params['AMOUNT'] > 100000) {
            return new EventResult(
                EventResult::ERROR,
                ['message' => 'Превышен лимит платежа']
            );
        }
        return new EventResult(EventResult::SUCCESS);
    }
);

Отладка событий

Логирование всех событий

php
<?php
// /local/php_interface/init.php
use Bitrix\Main\EventManager;
use Bitrix\Main\Diag\Debug;
if ($_GET['debug_events'] === 'Y' && $USER->IsAdmin()) {
    $em = EventManager::getInstance();
    // Логируем все события iblock
    foreach (['OnBeforeIBlockElementAdd', 'OnAfterIBlockElementAdd', 
              'OnBeforeIBlockElementUpdate', 'OnAfterIBlockElementUpdate'] as $eventName) {
        $em->addEventHandler('iblock', $eventName, function ($arFields) use ($eventName) {
            Debug::writeToFile([
                'event' => $eventName,
                'fields' => $arFields,
                'time' => date('Y-m-d H:i:s'),
                'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
            ], '', '/local/logs/events.log');
        }, false, 1); // Приоритет 1 — выполнится первым
    }
}

Поиск зарегистрированных обработчиков

php
<?php
use Bitrix\Main\EventManager;
$em = EventManager::getInstance();
// Получить все обработчики события
$handlers = $em->findEventHandlers('iblock', 'OnBeforeIBlockElementAdd');
echo '<pre>';
print_r($handlers);
echo '</pre>';
⚠️ Важно

Осторожно: Бесконечные циклы! Если в обработчике OnAfterIBlockElementUpdate вызвать CIBlockElement::Update(), событие сработает снова. Используйте флаги или static-переменные.

Защита от рекурсии

php
<?php
class IblockHandler
{
    private static bool $processing = false;

    public static function onAfterElementUpdate(array &$arFields): void
    {
        // Защита от рекурсии
        if (self::$processing) {
            return;
        }

        self::$processing = true;
        try {
            // Обновляем связанные элементы
            \CIBlockElement::Update($relatedId, ['PROPERTY_VALUES' => [...]]);
        } finally {
            self::$processing = false;
        }
    }
}
💡 Совет

У статического флага есть предел применимости. Он защищает от рекурсии внутри одного запроса, но выключает обработчик целиком: если во время обработки товара A нужно обновить товар B, и на B тоже должна отработать ваша логика, флаг её подавит. Более точный вариант — вести список уже обработанных ID (private static array $processed = []) и проверять конкретный элемент, а не факт «мы внутри обработчика».

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

Приоритеты обработчиков

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

// Выполнится ПЕРВЫМ (меньший приоритет = раньше)
$em->addEventHandler('iblock', 'OnBeforeIBlockElementAdd', $validationHandler, false, 1);
// Выполнится ВТОРЫМ
$em->addEventHandler('iblock', 'OnBeforeIBlockElementAdd', $enrichmentHandler, false, 100);
// Выполнится ПОСЛЕДНИМ
$em->addEventHandler('iblock', 'OnBeforeIBlockElementAdd', $loggingHandler, false, 500);

Итоги

ЗадачаРешение
ПодпискаEventManager::addEventHandler()
Свои событияnew Event()->send()
Отмена операцииreturn false или EventResult::ERROR
Изменение данныхМодификация &$arFields
ОтладкаЛогирование + findEventHandlers()

Правила, которые я проверяю на ревью

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

Обработчик в классе, а не в замыкании. Тестируемость, читаемый стек вызовов, ленивая загрузка файла.

Никаких тяжёлых операций внутри. Обработчик выполняется в критическом пути записи. Отправка письма, обращение к внешнему API, генерация PDF — всё это превращает обмен с 1С из десяти минут в четыре часа, а при недоступности внешнего сервиса ещё и роняет сохранение товара целиком. Тяжёлое — в очередь или в агент.

Защита от рекурсии — по конкретному элементу, а не глобальным флагом, и лучше через проверку «а изменилось ли что-то».

Работа без $USER и $APPLICATION. Тот же обработчик выполнится в cron-агенте и в консольном скрипте, где авторизованного пользователя нет, а глобальные объекты могут вести себя иначе. Всё, что нужно, берите из аргументов события.

Исключения обработаны. Необработанное исключение в OnAfter* прерывает операцию, которая уже совершилась, — данные сохранены, а пользователь видит ошибку и жмёт «сохранить» ещё раз.

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

ЗадачаРешение
Подписка в рамках запросаaddEventHandler() из init.php
Постоянная подписка модуляregisterEventHandler() при установке
Отмена операцииThrowException() + return false
Изменение данныхМодификация &$arFields в OnBefore*
Реакция после сохраненияOnAfter*, только лёгкие операции
🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.