Что такое события Битрикс и зачем они нужны
Когда к нам приходят с задачей «добавить проверку перед сохранением товара» или «отправлять данные в CRM после регистрации пользователя», первое, что хочется сделать — открыть файл модуля и дописать пару строк прямо в ядро. Мы так не делаем и никому не советуем: правка /bitrix/modules/ улетает при первом же обновлении, а отладка превращается в археологию. События Битрикс — это штатный механизм, который позволяет реагировать на действия в системе, не трогая ядро: обновление элемента инфоблока, регистрация пользователя, оформление заказа, сохранение файла.
Дальше в статье мы разберём, как подписаться на событие через init.php, как отменить сохранение из обработчика, какие события дают модули main, iblock, sale и fileman, и где искать зарегистрированные обработчики, когда что-то «не срабатывает».
Технически за регистрацию обработчиков отвечает класс EventManager — реализация паттерна Singleton, доступ через EventManager::getInstance(). Это D7-API, и мы рекомендуем использовать именно его: единообразный код, понятная отладка, никаких сюрпризов с сортировкой.
Но исторически в Битриксе есть и старое ядро со своими функциями — RegisterModuleDependences, UnRegisterModuleDependences, AddEventHandler, RemoveEventHandler, GetModuleEvents. Они никуда не делись и до сих пор встречаются в проектах. Ядро проксирует эти legacy-вызовы через метод addEventHandlerCompatible в EventManager — по опыту подрядчиков это работает прозрачно, но смешивать оба подхода в одном проекте не стоит.
Ключевое различие между двумя мирами — где живёт регистрация. RegisterModuleDependences пишет запись в таблицу b_module_to_module и вызывается один раз, обычно при установке модуля. AddEventHandler регистрирует обработчик на лету, на каждом хите, без записи в базу. Что смотреть в документации: раздел по EventManager для D7 и страницу функций старого ядра — там же параметры AddEventHandler (from_module_id, MESSAGE_ID, callback, sort, full_path).
Подробнее: ru.stackoverflow.
Подробнее: ru.stackoverflow.
Подробнее: ru.stackoverflow.
Подробнее: ru.stackoverflow.
Подробнее: init.php обработчик формы.
Подробнее: битрикс ошибка 500.
Как подписаться на событие в init.php
Типичный сценарий: в проекте нужно добавить проверку перед сохранением товара или отправлять данные в CRM после регистрации. Первое место, куда смотрит разработчик, — local/php_interface/init.php. Так исторически сложилось: файл подключается на каждом хите, доступен без установки модуля и правится за минуту. Для 80% задач этого хватает: небольшие правки поведения, валидация полей, логирование, отправка уведомлений.
Дальше два способа подписки: legacy-функция и D7-менеджер. Оба рабочие, но ведут себя по-разному.
<?php
// local/php_interface/init.php
AddEventHandler("iblock", "OnBeforeIBlockElementAdd", [
"CatalogHandler",
"onBeforeElementAdd"
]);
class CatalogHandler
{
public static function onBeforeElementAdd(&$arFields)
{
// замените на ваш IBLOCK_ID
if ((int)$arFields["IBLOCK_ID"] !== 12) {
return true;
}
if (empty($arFields["PROPERTY_VALUES"]["BARCODE"])) {
global $APPLICATION;
$APPLICATION->ThrowException("Не заполнен штрихкод товара");
return false;
}
return true;
}
}<?php
// local/php_interface/init.php
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
$eventManager->addEventHandler(
"iblock",
"OnBeforeIBlockElementAdd",
["CatalogHandler", "onBeforeElementAdd"],
100,
false
);
class CatalogHandler
{
public static function onBeforeElementAdd(&$arFields)
{
if ((int)$arFields["IBLOCK_ID"] !== 12) {
return true;
}
if (empty($arFields["PROPERTY_VALUES"]["BARCODE"])) {
global $APPLICATION;
$APPLICATION->ThrowException("Не заполнен штрихкод товара");
return false;
}
return true;
}
}Разберём параметры AddEventHandler. В D7-методе addEventHandler они те же, только full_path убран за ненадобностью. Первый аргумент — from_module_id: строка с именем модуля-источника события. Для главного модуля это "main", для инфоблоков — "iblock", для корзины — "sale", для структуры сайта — "fileman". Второй — MESSAGE_ID, идентификатор события, ровно тот, что стреляет в ядре: OnBeforeIBlockElementAdd, OnAfterUserRegister и так далее.
Третий параметр — callback: имя функции, массив [класс, метод] или замыкание. Мы предпочитаем массив с классом — так код не засоряет глобальную область и его проще тестировать. Четвёртый — sort, по умолчанию 100: чем меньше число, тем раньше выполнится обработчик. Если у вас несколько подписок на одно событие и порядок важен, задавайте явные значения (10, 50, 200). Пятый — full_path в legacy-версии: путь к файлу с функцией, если она не подключена. На практике почти всегда false: класс уже загружен через init.php.
Важно: если init.php не подключён (например, вы работаете в CLI-скрипте в обход пролога) или обработчик зарегистрирован уже после того, как событие отстреляло, ошибки не будет. Событие просто пройдёт мимо, обработчик не вызовется, а вы будете искать причину в коде хендлера. Проверяйте, что пролог подключён и подписка идёт до точки срабатывания — обычно сразу при загрузке init.php.
OnBeforeIBlockElementAdd: как отменить сохранение товара
Самый частый запрос на эту тему звучит так: «менеджеры сохраняют товар без артикула, и он уезжает в каталог — как запретить?». Особенно остро это встаёт при импорте из 1С: обмен гонит XML-выгрузку, часть позиций приходит с пустым PROPERTY_ARTICLE или с незаполненным UF-полем вроде UF_BARCODE, и без фильтра такие товары молча попадают на сайт. Через сутки их уже сотни, а покупатель видит карточку без идентификатора для заказа.
Логика тут простая: валидировать данные нужно до записи в базу, а не после. Именно для этого в модуле iblock предусмотрено событие OnBeforeIBlockElementAdd — оно стреляет до вставки элемента, получает массив $arFields и может отменить операцию. Комбинируется это с OnBeforeIBlockElementUpdate (для правок) и OnBeforeIBlockSectionAdd (для разделов), но в этой секции разберём именно добавление товара.
<?php
// local/php_interface/init.php
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'iblock',
'OnBeforeIBlockElementAdd',
'checkArticleBeforeAdd'
);
function checkArticleBeforeAdd(&$arFields)
{
// замените на ID вашего инфоблока каталога
if ((int)$arFields['IBLOCK_ID'] !== 17) {
return true;
}
$article = trim((string)($arFields['PROPERTY_VALUES']['ARTICLE'][0]['VALUE'] ?? ''));
if ($article === '') {
global $APPLICATION;
$APPLICATION->ThrowException('Артикул обязателен: товар без артикула не сохраняется.');
return false;
}
$barcode = trim((string)($arFields['UF_BARCODE'] ?? ''));
if ($barcode !== '' && !preg_match('/^[0-9]{8,14}$/', $barcode)) {
global $APPLICATION;
$APPLICATION->ThrowException('UF_BARCODE: ожидается 8–14 цифр.');
return false;
}
return true;
}Ключевое здесь — не сам return false, а пара «возврат false + $APPLICATION->ThrowException()». Многие разработчики останавливаются на первом шаге и потом удивляются: товар не сохраняется, но менеджер не видит никакой ошибки — форма просто перезагружается, будто ничего не произошло. Без ThrowException текст ошибки нигде не отображается: ядро откатывает операцию молча. Официальная документация прямо требует вызывать оба — возврат false для отмены и исключение для сообщения пользователю.
Текст из ThrowException оседает в сессии и выводится в админке над формой редактирования элемента — стандартным красным блоком, как при ошибках валидации самого Битрикса. В публичной части он показывается, если вы сами выводите компонент bitrix:system.show_message или используете $APPLICATION->GetException() — по умолчанию на фронте сообщение не появится. Поэтому при импорте из 1С текст исключения ищите в логах обмена: агент импорта ловит исключение и пишет его в свой журнал, а не показывает в браузере.
Под свой проект чаще всего меняют три вещи: IBLOCK_ID (или убирают проверку, если правило общее для всех каталогов), список обязательных свойств в $arFields['PROPERTY_VALUES'] и набор регулярных выражений для UF-полей. Если валидация нужна и при обновлении товара — продублируйте функцию и навесьте её на OnBeforeIBlockElementUpdate, иначе правка через админку обойдёт проверку.
События модулей Битрикс: main, iblock, sale, fileman
Пространство имён событий в Битриксе устроено просто, но именно на этом ломается половина регистраций у тех, кто впервые садится за EventManager. Идентификатор модуля — это первый аргумент при подписке, и он же часть «адреса» события. Когда мы пишем EventManager::getInstance()->addEventHandler('iblock', 'OnBeforeIBlockElementAdd', ...), мы говорим ядру: «слушай событие OnBeforeIBlockElementAdd, которое принадлежит модулю iblock». Перепутали модуль — обработчик зарегистрируется без ошибки, но никогда не выстрелит: нужный модуль бросает своё событие в другом пространстве имён.
У нас в практике это самая частая причина «мёртвых» хуков. Код в init.php выглядит рабочим, а поведение не меняется. Причина — модуль указан не тот.
Идентификатор модуля для главного модуля — main, для управления структурой — fileman, для инфоблоков — iblock, для магазина — sale. Эти четыре закрывают большинство бытовых задач: файлы, элементы каталога, корзина, пользователи и настройки.
| Модуль | Пример события | Когда стреляет | Можно ли отменить |
|---|---|---|---|
main |
Перед записью файла на диск | Нет | |
iblock |
OnBeforeIBlockElementAdd |
До добавления элемента инфоблока | Да, через false + ThrowException |
sale |
OnBeforeSaleBasketItemSetField |
Перед изменением поля позиции корзины | Да |
fileman |
OnBeforeFileDelete |
Перед удалением файла в модуле структуры | Да |
Как выбрать модуль? Смотрим, какой модуль владеет объектом, который нужно перехватить. Работаете с элементами каталога — iblock. Ловите момент регистрации пользователя или сохранения файла — main. Реагируете на изменение корзины, заказа или цены — sale. Управляете деревом структуры сайта — fileman.
Полный список событий модуля ищите в официальной документации: у каждого модуля есть раздел «События» с именами, аргументами и моментом вызова. Для main это отдельная страница с десятками хуков, для iblock — своя, и так далее.
По опыту подрядчиков, удобно держать под рукой таблицу b_module_to_module: она показывает, какие обработчики уже зарегистрированы и на какие модули они подписаны. Спасает, когда чужой модуль перехватывает событие раньше вашего и меняет данные до того, как до них доберётесь вы.
Если задача нестандартная и подходящего события в списке нет — не изобретайте новый хук. Скорее всего, нужный момент уже покрыт событием в одном из четырёх модулей выше.
Журнал событий Битрикс: CEventLog и таблица b_event_log
Распространённая ошибка: считать, что события нужны только для изменения поведения системы. половина их ценности — в обратную сторону: узнать, что уже произошло. Когда после ночного импорта из 1С часть товаров оказалась без цен, а часть — с задвоенными артикулами, никто не помнит, какой обработчик что делал в 3 часа ночи. Здесь и выручает CEventLog с его таблицей b_event_log.
Класс CEventLog позволяет решать три ключевые задачи. Разбор инцидентов: восстановление цепочки действий с помощью метода GetList, позволяющего отфильтровать записи по времени, модулю и объекту.
Всё это пишется в одну таблицу.
<?php
use Bitrix\Main\Loader;
Loader::includeModule('main');
// Запись события безопасности (используем стандартный тип аудита)
CEventLog::Log(
'SECURITY', // SEVERITY
'USER_LOGIN_FAILURE', // AUDIT_TYPE_ID
'main', // MODULE_ID
$USER->GetID(), // ITEM_ID
'Попытка входа с неверным паролем' // DESCRIPTION
);
// Запись произвольного события (например, при работе с инфоблоками)
CEventLog::Log(
'INFO', // SEVERITY
'MY_CUSTOM_EVENT', // AUDIT_TYPE_ID (собственный ID)
'iblock', // MODULE_ID
$elementId, // ITEM_ID
'Завершена обработка данных' // DESCRIPTION
);Метод CEventLog::Log() принимает уровень SEVERITY, тип события, модуль и текстовое описание. Уровни стандартные: SECURITY — для всего, что касается доступа и подозрительной активности; ERROR — сбои, которые сломали сценарий; WARNING — нештатное, но не критичное; INFO — рабочие вехи вроде завершённого импорта; DEBUG — отладочный шум, который на проде обычно отключают.
С версии 15.5.9 ключи REMOTE_ADDR, USER_AGENT, REQUEST_URI, USER_ID и GUEST_ID система переопределяет сама — подставлять их руками бессмысленно, значения затрутся. Удобно: не нужно вытаскивать IP из $_SERVER и тащить ID пользователя через весь код. А вот AUDIT_TYPE и MODULE_ID — ваша зона ответственности. От них зависит, как вы потом будете фильтровать записи в админке и в SQL-запросах.
Чистить журнал удобнее агентом, а не руками. Рабочий вариант:
Где посмотреть зарегистрированные обработчики: GetModuleEvents и b_module_to_module
Распространённая ситуация: в проекте кто-то из прошлых подрядчиков подписался на OnBeforeIBlockElementUpdate, и теперь при сохранении товара уезжает не то поле. Или наоборот — нужно понять, откуда в заказе появляется значение, которого нет в шаблоне. В обоих случаях первым делом надо увидеть полный список обработчиков события: кто, с каким приоритетом, из какого модуля. У нас в практике это стандартный шаг диагностики перед тем, как что-то править в init.php. Иначе легко добавить второй обработчик поверх существующего и получить двойной вызов.
Штатный инструмент — функция GetModuleEvents из старого ядра. Она возвращает список зарегистрированных обработчиков конкретного события в виде объекта CDBResult, то есть результат можно перебрать в цикле и распечатать.
Покажем, как это выглядит на живом примере.
Третий аргумент true — ключевой момент. Без него функция вернёт только обработчики, зарегистрированные прямо сейчас в текущем запросе через AddEventHandler. С ним — весь список, включая записи из базы. Меняя первый аргумент, вы точно так же смотрите любое другое событие: 'sale', 'main', 'fileman'.
Разница между двумя способами регистрации — источник половины путаницы. Обработчики «на лету», добавленные через AddEventHandler (или D7-метод EventManager::addEventHandler), живут только в рамках текущего запроса и исчезают после его завершения. Их видно в GetModuleEvents только при full_path = false и только в том же запросе, где они были добавлены.
Обработчики, записанные в базу через RegisterModuleDependences, живут постоянно — их вы найдёте в таблице b_module_to_module в базе данных. Именно там хранятся зависимости между модулями: какой модуль на какое событие подписан, с каким приоритетом и какой функцией. По опыту подрядчиков, b_module_to_module — самый быстрый способ увидеть всю картину подписок сразу, без запуска PHP: достаточно открыть таблицу в админке БД или через SQL-запрос. А вот GetModuleEvents удобнее, когда нужно посмотреть конкретное событие в контексте выполнения и заодно отладить порядок вызова.
Самые частые ошибки при работе с событиями Битрикс
Распространённая ошибка: разработчик пишет обработчик, кладёт его в init.php, обновляет страницу — и ничего не происходит. Через час выясняется, что обработчик рабочий, просто кеш сайта не сбросили. За годы работы с Битриксом мы собрали небольшой список грабель, на которые наступают почти все.
Основных причин, по которым «событие не срабатывает», всего четыре. Разберём их в таблице — от самой частой к самой коварной.
| Грабля | Причина | Решение |
|---|---|---|
Обработчик в init.php не срабатывает |
Не сброшен кеш сайта | Очистить кеш в админке или bitrix/cache |
OnAfter выстрелил при откате OnBefore |
Событие OnAfter* вызывается всегда — даже если OnBefore* бросил исключение |
Проверять в OnAfter, что запись реально сохранилась |
RegisterModuleDependences не пишет в БД |
Вызов должен идти при установке модуля, а не на каждом хите | Для рантайма — AddEventHandler или EventManager |
| Обработчик зарегистрирован после события | Регистрация должна произойти до вызова события | Подключать в init.php, а не в теле страницы |
Подробнее об услуге: разработка корпоративных сайтов на Битрикс.
Частые вопросы
Можно ли подписаться на событие из init.php без регистрации модуля?
Да, AddEventHandler и EventManager::getInstance()->addEventHandler работают из init.php без собственного модуля. Обработчик должен быть доступен на момент вызова — обычно это функция или статический метод класса, подключённого через require в том же init.php.
Что делать, если обработчик OnBeforeIBlockElementAdd не срабатывает при импорте из 1С?
Проверьте, что событие регистрируется именно для нужного модуля iblock и что импорт не идёт через прямой SQL или обход API. При обмене через CommerceML элементы часто добавляются с флагом, а часть операций идёт через OnBeforeIBlockSectionAdd или агенты — смотрите b_module_to_module и лог CEventLog.
Чем отличается AddEventHandler от EventManager::getInstance()->addEventHandler в D7?
AddEventHandler — обёртка над старым API, работает с именами модуля и события в виде строк. EventManager D7 принимает те же параметры, но поддерживает сортировку, возврат значений и корректную работу с namespace; для нового кода предпочтителен D7-вариант.
А если у меня несколько обработчиков на одно событие — в каком порядке они вызовутся?
Порядок задаётся параметром sort в AddEventHandler (по умолчанию 100) — чем меньше число, тем раньше вызов. При равном sort порядок регистрации сохраняется, но полагаться на него не стоит: задавайте явные значения.
Можно ли из обработчика OnBeforeIBlockElementAdd вернуть товар с ошибкой, чтобы 1С получила понятный текст?
Да, установите $APPLICATION->ThrowException('текст') и верните false — исключение попадёт в результат обмена. Для CommerceML текст уйдёт в ответ как сообщение об ошибке импорта.
Как быстро найти, кто именно вешает обработчик на OnBeforeIBlockElementAdd, если в проекте десятки модулей?
Выполните EventManager::getInstance()->findEventHandlers('iblock', 'OnBeforeIBlockElementAdd') — вернёт массив с module, class, method и sort. Альтернативно смотрите таблицу b_module_to_module через SQL-запрос по полю EVENT.