Play
online · 30 мин
Туториалы 12 мин чтения 13.09.2026

События Битрикс: init.php, OnBeforeIBlockElementAdd, журнал

Правка ядра Битрикс обернётся проблемами при обновлении. Разбираем, как решать задачи через события: валидация товаров, логирование, интеграция с CRM. Плюс — где искать события и как подписаться в init.php.

Что такое события Битрикс и зачем они нужны

Когда к нам приходят с задачей «добавить проверку перед сохранением товара» или «отправлять данные в 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
<?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
<?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
<?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
<?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-запросах.

Чистить журнал удобнее агентом, а не руками. Рабочий вариант:

PHP

Где посмотреть зарегистрированные обработчики: GetModuleEvents и b_module_to_module

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

Штатный инструмент — функция GetModuleEvents из старого ядра. Она возвращает список зарегистрированных обработчиков конкретного события в виде объекта CDBResult, то есть результат можно перебрать в цикле и распечатать.

Покажем, как это выглядит на живом примере.

PHP

Третий аргумент 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.

#Bitrix #PHP #Bitrix модули
автор · Backend / SRE Engineer
Артем Колячек

Backend-разработчик и SRE в студии Paradigma. Занимается тем, что у других проектов обычно обнаруживается за неделю до запуска: переездом Bitrix-проектов между серверами, починкой кешей после миграций, выстраиванием pipeline для контейнеров, мониторингом под нагрузкой.

До Paradigma — 7 лет в backend (PHP + Postgres) и в operations (Linux, Docker, Coolify, restic-бэкапы). Любит когда логи разговаривают полным синтаксисом ошибки, а не «что-то пошло не так».

На блоге пишет ровно про те ситуации с которыми сам разбирался руками: какой Bitrix-апдейт сломал кеш и как откатить, почему Docker-сеть не находит контейнер после рекрейта, что делать когда asyncpg ругается на event loop.