Перейти к содержимому

События, обработчики и агенты в 1С-Битрикс

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

Как это работает

Событие - это точка расширения. Модуль в нужный момент генерирует событие с именем вроде OnBeforeUserAdd или OnAfterIBlockElementUpdate и передаёт данные. Все зарегистрированные обработчики вызываются по очереди и могут изменить данные, отменить операцию или добавить побочный эффект. Именно так расширяют платформу: код модулей править нельзя, а события и компоненты - легальные точки входа.

Моделей две, и они сосуществуют. В старом ядре обработчик регистрируют функцией AddEventHandler() (на текущий хит) или RegisterModuleDependences() (постоянно, в базе), а инициатор сам перебирает обработчики через GetModuleEvents() и ExecuteModuleEvent(). Обработчик получает позиционные аргументы, часть - по ссылке, и возврат false в событии OnBefore* отменяет операцию. В D7 единая точка входа - EventManager::getInstance(), событие это объект Event, а ответ обработчика - объект EventResult.

Четыре D7-метода регистрации различаются по двум осям. По времени жизни: registerEventHandler() пишет обработчик в базу (регистрируют один раз при установке модуля, снимают при удалении), а addEventHandler() живёт только текущий хит и обычно ставится в init.php. По формату аргументов: обычные методы передают в обработчик объект Event, а варианты с суффиксом Compatible

  • старые позиционные аргументы. Для старых событий ядра, у которых объекта Event просто нет, нужен именно Compatible-вариант.

Имя события говорит о его роли. События OnBefore* срабатывают до действия, и возврат false или EventResult::ERROR отменяет операцию, а поля часто передаются по ссылке и их можно подменить. События OnAfter* и On* - завершающие. Отдельная группа - события точек страницы: OnPageStart, OnBeforeProlog, OnProlog, OnEpilog, OnAfterEpilog.

У ORM своя событийная система. Для сущностей DataManager работает отдельный менеджер Bitrix\Main\ORM\EventManager с константами вида DataManager::EVENT_ON_BEFORE_ADD. Важно: события инфоблоков старого ядра этим механизмом не покрываются, для них по-прежнему нужен обычный EventManager.

Три способа выполнить работу не в основном потоке. Агент (CAgent) - это регулярная задача по расписанию, которая по умолчанию запускается на хитах посетителей, но переносится на cron. Фоновая задача addBackgroundJob() выполняется после отправки ответа пользователю. Очередь сообщений (Messenger) - для асинхронной обработки с отдельным потребителем.

Примеры

1. D7: подписка, отправка, разбор результата

$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler('mymodule', 'OnMacrosProductCreate', 'OnMacrosProductCreate');
function OnMacrosProductCreate(\Bitrix\Main\Event $event) {
$params = $event->getParameters();
return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS, $basketId);
}
// сторона инициатора
$event = new \Bitrix\Main\Event('mymodule', 'OnMacrosProductCreate', [$basketId]);
$event->send();
foreach ($event->getResults() as $eventResult) {
if ($eventResult->getResultType() === \Bitrix\Main\EventResult::ERROR) {
return false; // обработчик запретил операцию
}
}

Обработчик получает объект события и возвращает EventResult с типом SUCCESS, ERROR или UNDEFINED. Параметры события можно передавать замыканием - тогда тяжёлые вычисления не выполнятся, если подписчиков нет.

2. Отмена операции из обработчика

final class BeforeTicketCloseEventHandler
{
public static function handle(BeforeTicketCloseEvent $event): \Bitrix\Main\EventResult
{
if (self::hasOpenTasks($event->ticketId)) {
return new \Bitrix\Main\EventResult(
\Bitrix\Main\EventResult::ERROR,
parameters: ['message' => 'У заявки есть незакрытые задачи'],
moduleId: 'my.taskTracker',
);
}
return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS);
}
}

В старой модели то же самое выглядит иначе - обработчик просто возвращает false, а текст ошибки кладёт в глобальное приложение:

AddEventHandler('main', 'OnBeforeUserAdd', 'checkUserBeforeAdd');
function checkUserBeforeAdd(&$fields) {
if (empty($fields['EMAIL'])) {
global $APPLICATION;
$APPLICATION->ThrowException('Не указан email');
return false; // операция отменена
}
$fields['NAME'] = trim($fields['NAME']); // поле придёт по ссылке - можно править
}

Для таких событий в D7-стиле регистрация должна идти через addEventHandlerCompatible() или registerEventHandlerCompatible() - иначе обработчик получит объект Event вместо ожидаемых аргументов и просто не увидит данные.

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

class MyHandler
{
protected static $handlerDisallow = false;
public static function iblockElementUpdateHandler(&$fields)
{
if (self::$handlerDisallow) {
return;
}
self::$handlerDisallow = true;
CIBlockElement::Update($fields['ID'], ['ACTIVE' => 'N']);
self::$handlerDisallow = false;
}
}

Классическая ошибка: обработчик события OnAfterIBlockElementUpdate сам вызывает CIBlockElement::Update(), тот снова поднимает событие, и всё заканчивается 500-й ошибкой. Статический флаг решает проблему. Для массовых операций он обязателен - before- и after-события могут отработать вперемешку.

4. ORM-событие сущности инфоблока

use Bitrix\Main\ORM\Data\DataManager;
$iblock = \Bitrix\Iblock\Iblock::wakeUp(32);
$em = \Bitrix\Main\ORM\EventManager::getInstance();
$em->registerEventHandler(
$iblock->getEntityDataClass(),
DataManager::EVENT_ON_BEFORE_ADD,
'mymodule',
'MyClass',
'method'
);

Здесь всё своё: отдельный менеджер, константы событий и класс сущности, который получают через wakeUp(). Если нужно поймать изменение элемента, сделанное старым API, подписываться придётся на события старого ядра.

5. Агент

// раз в сутки, непериодический
CAgent::AddAgent('CStatistic::CleanUpStatistics_2();', 'statistic', 'N', 86400);
// агент, повторяющий сам себя
function MyAgentFunction() {
// полезная работа
return 'MyAgentFunction();'; // строка со своим вызовом = следующий запуск
}
// агент, который отработает 7 раз и удалится
class CMyModule {
public static function Agent007($cnt = 1): string {
if ($cnt >= 7) {
return ''; // пустая строка = самоудаление
}
return 'CMyModule::Agent007(' . ($cnt + 1) . ');';
}
}

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

6. Перенос агентов на cron

По умолчанию агенты выполняются на хитах посетителей: нет трафика - нет выполнения, а долгий агент тормозит чужой запрос. На боевом проекте их переносят на cron.

// 1. отключить запуск на хитах
\Bitrix\Main\Config\Option::set('main', 'agents_use_crontab', 'N');
\Bitrix\Main\Config\Option::set('main', 'check_agents', 'N');
// 2. в /bitrix/php_interface/dbconn.php
if (!(defined('CHK_EVENT') && CHK_EVENT === true)) {
define('BX_CRONTAB_SUPPORT', true);
}
// 3. создать /bitrix/php_interface/cron_events.php и вызвать в нём CAgent::CheckAgents();
*/1 * * * * /usr/bin/php -f /path/to/cron_events.php > /dev/null 2>&1

PHP в командной строке должен работать от того же пользователя и с теми же настройками, что и веб-сервер, иначе начнутся проблемы с правами на файлы кеша.

7. Фоновая задача после отдачи страницы

$app = \Bitrix\Main\Application::getInstance();
$app->addBackgroundJob(
[FileProcessor::class, 'processLargeFile'],
[
'path' => $tmpFilePath,
'original_name' => $fileName,
],
\Bitrix\Main\Application::JOB_PRIORITY_LOW
);

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

Справочник API

События

APIНазначениеОсобенности
EventManager::registerEventHandler()постоянный обработчикхранится в базе, снимается unRegisterEventHandler()
EventManager::addEventHandler()обработчик на текущий хитобычно в init.php, базу не нагружает
*Compatible-варианты обоих методовстарый формат аргументовнужны для событий без объекта Event
AddEventHandler()легаси-регистрация на хитвстречается в старом коде
RegisterModuleDependences()легаси-регистрация в базеаналог registerEventHandler
new Event($module, $name, $params) + send()отправка событияпараметры можно задать замыканием
$event->getParameter() / getParameters()доступ к данным события
EventResultответ обработчикатипы SUCCESS, ERROR, UNDEFINED
ORM\EventManagerсобытия ORM-сущностейконстанты DataManager::EVENT_ON_*

Фоновая работа

APIНазначениеОсобенности
CAgent::AddAgent()регистрация агентапараметры: вызов, модуль, периодичность, интервал
возврат строки из агентаследующий запускпустая строка удаляет агента
CAgent::CheckAgents()запуск очереди агентоввызывается из cron-скрипта
agents_use_crontab, check_agentsнастройки переноса на cronотключают запуск на хитах
BX_CRONTAB_SUPPORTконстанта в dbconn.phpвключает cron-режим
Application::addBackgroundJob()задача после отдачи ответапринимает callable и массив аргументов, есть приоритеты
Messengerочереди сообщенийсвоё сообщение, свой получатель, конфигурация транспорта
CEvent::Send() / SendImmediate()почтовые уведомленияпервый кладёт письмо в очередь, второй шлёт сразу

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

Рекурсия обработчика и ошибка 500. Симптом: страница падает при сохранении элемента. Обработчик события записи вызывает тот же метод записи. Лечится статическим флагом блокировки в начале обработчика.

Перепутан обычный метод регистрации и Compatible. Симптом: обработчик вызывается, но данные внутри пустые или не того типа. Обычные методы передают объект Event, Compatible-варианты - позиционные аргументы. Для старых событий ядра нужен второй вариант.

Агент отработал один раз и пропал. Симптом: задача выполнилась и больше не запускается. Функция агента не вернула строку со своим вызовом.

Агенты не выполняются вообще. На сайтах без трафика агенты, привязанные к хитам, просто не запускаются. Решение - перенести их на cron.

Условия по $_SESSION в init.php не работают. Сессия инициализируется после подключения этого файла. Запускать её вручную там нельзя.

Создали init.php в /local, и старый код перестал работать. При наличии файла в /local/php_interface/ файл в /bitrix/php_interface/ больше не подключается. Код нужно перенести, а не продублировать.

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

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

Где регистрировать обработчик - в init.php или в модуле?

Разовые обработчики и точки расширения удобно вешать в /local/php_interface/init.php через addEventHandler() - они живут один хит и не нагружают базу. Обработчики собственного модуля правильнее регистрировать один раз при установке через registerEventHandler(): они хранятся в базе, их легко найти и они автоматически снимаются при удалении модуля. Тяжёлую логику в init.php держать не стоит - ошибка там роняет сайт целиком.

Как отменить операцию из обработчика?

В старой модели обработчик события OnBefore возвращает false, а текст ошибки кладёт через $APPLICATION->ThrowException(). В D7 обработчик возвращает EventResult с типом ERROR, а инициатор разбирает результаты циклом по getResults(). События с именем OnAfter отменить уже нельзя - действие произошло.

Почему обработчик изменения элемента не срабатывает при записи через ORM?

Потому что это две независимые событийные системы. У ORM-сущностей свой менеджер ORM\EventManager и свои константы событий, а события инфоблоков старого ядра им не покрываются. Если данные могут меняться и старым API, и через ORM, подписываться нужно на обе стороны.

Агент, фоновая задача или очередь - что выбрать?

Агент - для регулярной работы по расписанию: очистка, пересчёт, выгрузка. Фоновая задача через addBackgroundJob() - для работы, которая относится к текущему запросу, но не должна задерживать ответ: отправка письма, обработка загруженного файла. Очередь сообщений - когда обработка асинхронная и её выполняет отдельный потребитель, с ретраями и контролем нагрузки.

Обязательно ли переносить агенты на cron?

На боевом проекте - да. По умолчанию агенты запускаются на хитах посетителей: на сайте без трафика они не выполнятся вовсе, а тяжёлый агент затормозит запрос случайного пользователя. Перенос сводится к отключению двух настроек, константе BX_CRONTAB_SUPPORT в dbconn.php и вызову CAgent::CheckAgents() из cron-скрипта. Следите, чтобы PHP в CLI работал от того же пользователя, что и веб-сервер.

Связанные темы

Первоисточники