Подключить события Bitrix¶
В проекте на Bitrix обработчики событий обычно живут в init.php или events.php: строка AddEventHandler(...) за строкой, классы и методы — строками, &$arFields — по ссылке, отмена — через return false и $APPLICATION->ThrowException(). Пока обработчиков десять, это терпимо. Когда их сотня, уже никто не скажет, что именно произойдёт при сохранении товара.
Мост BitrixEventBridge переносит события Bitrix в тот же файл маршрутов, где живут ваши собственные события. Слушатели, middleware, порядок вызова, лог и обработка ошибок — те же самые.
1. Опишите маршруты¶
События Bitrix — в группе bitrix, топик — <модуль>.<Событие>, как в Bitrix:
use Selyusize\EventsRouter\EventRouter;
use Selyusize\EventsRouter\Routing\RouteGroup;
$routes = static function (EventRouter $events): void {
$events->group('bitrix', static function (RouteGroup $bitrix): void {
$bitrix->listen('iblock.OnBeforeIBlockElementUpdate', ValidateProduct::class);
$bitrix->listen('sale.OnSaleOrderPaid', MarkOrderPaid::class);
});
};
Каждая строка — одно конкретное событие. Шаблоны вроде sale.* здесь не работают: EventManager Bitrix подписывается только на точные пары «модуль + событие». Такой маршрут даст InvalidRoute при подключении моста.
2. Напишите слушателей¶
Слушатель — обычный ListenerInterface. Данные события Bitrix лежат в payload — объекте BitrixEvent:
use Selyusize\EventsRouter\Bitrix\BitrixEvent;
use Selyusize\EventsRouter\Contract\Core\EventInterface;
use Selyusize\EventsRouter\Contract\Core\ListenerInterface;
use Webmozart\Assert\Assert;
// Старый API: поля приходят по ссылке, их можно поправить или отменить сохранение
final class ValidateProduct implements ListenerInterface
{
public static function handle(EventInterface $event): void
{
$bitrix = $event->getPayload();
Assert::isInstanceOf($bitrix, BitrixEvent::class);
$name = trim((string)($bitrix->getFields()['NAME'] ?? ''));
if ($name === '') {
$bitrix->cancel('Название товара обязательно');
return;
}
$bitrix->setField('NAME', $name);
}
}
// D7: данные — в объекте Bitrix\Main\Event
final class MarkOrderPaid implements ListenerInterface
{
public static function handle(EventInterface $event): void
{
$bitrix = $event->getPayload();
Assert::isInstanceOf($bitrix, BitrixEvent::class);
echo 'Заказ ', $bitrix->getD7Event()?->getParameter('ORDER_ID'), ' оплачен', PHP_EOL;
}
}
Bitrix вызывает обработчики двумя способами. BitrixEvent понимает оба:
| Как Bitrix вызвал событие | Чем читать | Как отменить |
|---|---|---|
старый API: GetModuleEvents(), &$arFields |
getFields(), setField(), getArguments() |
cancel($reason) → Bitrix получит false, причина — в $APPLICATION->GetException() |
D7: Bitrix\Main\Event::send() |
getD7Event() |
cancel($reason) → Bitrix получит EventResult::ERROR с причиной в параметрах |
Какой стиль у конкретного события, смотрите в документации Bitrix к нему. Если перепутать, getFields() у события D7 честно скажет об этом исключением, а не вернёт пустой массив.
3. Подключите мост¶
В проекте — одна строка в local/php_interface/include/events.php:
use Bitrix\Main\EventManager;
use Rasa\Container\Container;
use Selyusize\EventsRouter\Bitrix\BitrixEventBridge;
use Selyusize\EventsRouter\EventRouter;
BitrixEventBridge::attachLazy(
EventManager::getInstance(),
static fn (): EventRouter => Container::get(EventRouter::class),
$_SERVER['DOCUMENT_ROOT'] . '/local/var/cache/bitrix-events.php',
);
init.php выполняется на каждом хите сайта. Собирать на каждом хите роутер, контейнер и все маршруты ради того, чтобы подписаться на пару событий, — дорого. Поэтому attachLazy():
- берёт список событий из файла и подписывается только на них;
- собирает роутер, только когда событие Bitrix действительно произошло;
- если файла нет, собирает роутер сразу и записывает список. Папка создаётся сама.
После изменения маршрутов файл нужно удалить, например на деплое. Иначе мост не узнает о новых событиях.
Для консольных скриптов и разработки есть attach() — подписка сразу по маршрутам роутера, без файла:
use Bitrix\Main\EventManager;
use Selyusize\EventsRouter\Bitrix\BitrixEventBridge;
use Selyusize\EventsRouter\EventRouterFactory;
$events = EventRouterFactory::create();
$routes($events);
BitrixEventBridge::attach(EventManager::getInstance(), $events);
Что получится¶
Bitrix вызывает события так же, как всегда, — мост ничего в нём не меняет:
use Bitrix\Main\Event as D7Event;
// Так Bitrix вызывает старое событие перед сохранением элемента инфоблока
$arFields = ['NAME' => ' Чайник '];
foreach (GetModuleEvents('iblock', 'OnBeforeIBlockElementUpdate', true) as $arEvent) {
ExecuteModuleEventEx($arEvent, [&$arFields]);
}
echo 'Название после слушателя: "', $arFields['NAME'], '"', PHP_EOL;
// Пустое название — слушатель отменяет сохранение
$arFields = ['NAME' => ''];
foreach (GetModuleEvents('iblock', 'OnBeforeIBlockElementUpdate', true) as $arEvent) {
if (ExecuteModuleEventEx($arEvent, [&$arFields]) === false) {
echo 'Сохранение отменено: ', $APPLICATION->GetException()->GetString(), PHP_EOL;
}
}
// А так — событие D7
(new D7Event('sale', 'OnSaleOrderPaid', ['ORDER_ID' => 42]))->send();
Название после слушателя: "Чайник"
Сохранение отменено: Название товара обязательно
Заказ 42 оплачен
Свои события рядом с событиями Bitrix¶
Событий Bitrix проекту обычно мало: «заказ оплачен» в бизнес-смысле, «бонусы начислены», «выгрузка завершена» Bitrix не вызывает. Такие события объявляются в том же файле маршрутов, рядом с группой bitrix, а рассылает их BitrixEventBridge::dispatch():
use Selyusize\EventsRouter\EventRouter;
use Selyusize\EventsRouter\Routing\RouteGroup;
$routes = static function (EventRouter $events): void {
// События Bitrix
$events->group('bitrix', static function (RouteGroup $bitrix): void {
$bitrix->listen('sale.OnSaleOrderPaid', TranslateOrderPaid::class);
});
// Свои события проекта
$events->group('shop', static function (RouteGroup $shop): void {
$shop->listen('order.{order_id:\d+}.paid', SendReceipt::class);
$shop->listen('order.{order_id:\d+}.paid', AccrueBonus::class);
});
};
dispatch() отправляет событие в тот же роутер, к которому подключён мост: маршруты, middleware, лог и обработка ошибок общие. Вызывать его можно отовсюду — из компонента, контроллера, агента и из слушателя события Bitrix. Последнее — хороший приём: слушатель переводит событие Bitrix на язык домена, и бизнес-логика вообще не знает о Bitrix:
use Selyusize\EventsRouter\Bitrix\BitrixEvent;
use Selyusize\EventsRouter\Bitrix\BitrixEventBridge;
use Selyusize\EventsRouter\Contract\Core\EventInterface;
use Selyusize\EventsRouter\Contract\Core\ListenerInterface;
use Selyusize\EventsRouter\Event;
use Webmozart\Assert\Assert;
// Переводит событие Bitrix на язык домена: дальше бизнес-логика о Bitrix не знает
final class TranslateOrderPaid implements ListenerInterface
{
public static function handle(EventInterface $event): void
{
$bitrix = $event->getPayload();
Assert::isInstanceOf($bitrix, BitrixEvent::class);
$orderId = (int)$bitrix->getD7Event()?->getParameter('ORDER_ID');
BitrixEventBridge::dispatch(new Event('shop.order.' . $orderId . '.paid'));
}
}
final class SendReceipt implements ListenerInterface
{
public static function handle(EventInterface $event): void
{
echo 'Чек по заказу ', $event->getAttribute('order_id'), ' отправлен', PHP_EOL;
}
}
final class AccrueBonus implements ListenerInterface
{
public static function handle(EventInterface $event): void
{
echo 'Бонусы за заказ ', $event->getAttribute('order_id'), ' начислены', PHP_EOL;
}
}
use Bitrix\Main\Event as D7Event;
// Bitrix сообщает об оплате — слушатель переводит событие в shop.order.42.paid
(new D7Event('sale', 'OnSaleOrderPaid', ['ORDER_ID' => 42]))->send();
// Своё событие из компонента, контроллера или агента — без события Bitrix
$report = BitrixEventBridge::dispatch(new Event('shop.order.43.paid'));
echo 'Слушателей: ', count($report->getListeners()), PHP_EOL;
Чек по заказу 42 отправлен
Бонусы за заказ 42 начислены
Чек по заказу 43 отправлен
Бонусы за заказ 43 начислены
Слушателей: 2
Что важно знать о dispatch():
- Роутер собирается один раз на процесс. С
attachLazy()— при первом событии, своём или Bitrix. Если за хит не произошло ни одного события, роутер не собирается вовсе. - Сначала подключите мост.
dispatch()доattach()илиattachLazy()бросаетLogicException: роутера, в который отправить событие, ещё нет. В проекте мост подключается вinit.php, так что к компонентам и контроллерам он уже готов. - Свои события — не под префиксом
bitrix. Событияbitrix.*вызывает сам Bitrix, и слушатели этой группы ждутBitrixEventв payload.dispatch(new Event('bitrix.main.OnAfterUserAdd'))броситLogicException. Если нужно вызвать событие Bitrix, вызовите его через Bitrix:(new Bitrix\Main\Event('sale', 'OnSaleOrderPaid', [...]))->send(). - Не берите роутер из контейнера без определения.
Container::get(EventRouter::class)в контейнере с автосвязыванием, где роутер не зарегистрирован, создаст новый пустой роутер без маршрутов: событие молча никуда не уйдёт.dispatch()такой ошибки не допускает.
Посмотреть все обработчики события¶
Слушатели роутера — не единственные, кто подписан на событие Bitrix. Рядом работают обработчики из init.php, из модулей ядра и партнёрских модулей. getHandlers() показывает их все в том порядке, в каком их вызовет Bitrix — список берётся из EventManager, а не из маршрутов:
$bitrix->getHandlers();
// [
// 'CLists::OnBeforeIBlockElementUpdate (lists)', // модуль из b_module_to_module
// 'App\\Catalog\\Listener\\NormalizeElement\\Handler::handle',
// 'Selyusize\\EventsRouter\\Bitrix\\BitrixEventBridge::bitrix.iblock.OnBeforeIBlockElementUpdate',
// 'updatePreviewText', // функция
// 'Closure /local/php_interface/init.php:42',
// ]
Мост роутера тоже в списке: по его месту видно, какие обработчики сработают до слушателей роутера, а какие — после. Удобно писать список в лог из middleware группы bitrix, когда нужно понять, кто ещё трогает поля при сохранении товара.
Если id модуля с точкой¶
У партнёрских модулей id содержит точку: rasa.shop. А точка — разделитель сегментов топика. Для таких модулей соберите топик через topic(), она закодирует точку как ~:
$bitrix->listen(BitrixEventBridge::topic('rasa.shop', 'OnOrderExport'), ExportOrder::class);
// топик: bitrix.rasa~shop.OnOrderExport
Если у роутера есть префикс¶
Мост ищет маршруты, топик которых начинается с bitrix.. Если роутеру задан setPrefix('shop'), полный топик станет shop.bitrix.main.OnAfterUserAdd — передайте префикс мосту последним аргументом: attach($manager, $events, 'shop.bitrix').
Что важно помнить¶
- Ошибка слушателя не роняет страницу. По умолчанию исключение попадает в отчёт и в лог роутера, остальные слушатели работают. Отменить действие можно только явно — через
cancel(). - После
cancel()остальные слушатели не вызываются.BitrixEventреализует PSR-14StoppableEventInterface, в отчёте у них статусSkipped. setField()меняет данные Bitrix. Это осознанное исключение из неизменяемости событий: вOnBefore*весь смысл в том, чтобы поправить поля. Изменения одного слушателя видят следующие.- Свои события — через
BitrixEventBridge::dispatch(). Тот же роутер, те же маршруты, см. Свои события рядом с событиями Bitrix.