События, обработчики и агенты в 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.phpif (!(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>&1PHP в командной строке должен работать от того же пользователя и с теми же настройками, что и веб-сервер, иначе начнутся проблемы с правами на файлы кеша.
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 работал от того же пользователя, что и веб-сервер.
Связанные темы
- Ядро D7 - Event, EventResult и остальные базовые классы
- Бизнес-процессы - визуальные сценарии на событиях
- D7 ORM - события сущностей
- Обработчик события: регистрация, аргументы, отмена действия - подписка и отмена действия
- Журнал событий: чтение, очистка, свои записи - куда писать следы фоновых процессов
- Задача в бизнес-процессе: назначение, сроки, ответ участника - согласования на практике
- PHP-код в бизнес-процессе: переменные, типы, отладка - код внутри процесса и его границы
- Раздел Ядро D7