События — то место, где Битрикс из «коробки с компонентами» превращается в расширяемую платформу. И то же самое место, где чаще всего ломают чужие проекты: обработчик в init.php выполняется на каждом сохранении элемента — из админки, из обмена с 1С, из агента, из консольного скрипта. Автор писал его под свой сценарий, а работает он во всех.
Разберём механику, а главное — правила, которые превращают обработчик из мины замедленного действия в предсказуемый код.
Как работают события
Событийная модель — основа расширяемости Битрикс. Ядро и модули генерируют события в ключевых точках, а ваш код может на них подписаться.
Действие пользователя
↓
Ядро Битрикс вызывает OnBefore*
↓
Ваш обработчик (можно отменить/изменить)
↓
Ядро выполняет действие
↓
Ядро вызывает OnAfter*
↓
Ваш обработчик (логирование, уведомления)OnBefore* — можно отменить операцию или изменить данные. OnAfter* — операция уже выполнена, только реагируем.
Подписка на события
Через init.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
// /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
// /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
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
// Регистрация
$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
// Создание заказа
$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
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
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
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
// /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
use Bitrix\Main\EventManager;
$em = EventManager::getInstance();
// Получить все обработчики события
$handlers = $em->findEventHandlers('iblock', 'OnBeforeIBlockElementAdd');
echo '<pre>';
print_r($handlers);
echo '</pre>';Осторожно: Бесконечные циклы! Если в обработчике OnAfterIBlockElementUpdate вызвать CIBlockElement::Update(), событие сработает снова. Используйте флаги или static-переменные.
Защита от рекурсии
<?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
$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.