Events
Events позволяет нескольким частям PHP-приложения реагировать на одно действие. Например, после сохранения заказа можно обновить метрики и подготовить уведомление, не связывая эти обработчики между собой.
Все обработчики выполняются синхронно в том же процессе, который вызвал emit(). Регистрации хранятся в статических массивах. Отдельного сервера, транспорта и фонового выполнения нет.
Подключение
Заголовок раздела «Подключение»Нужен PHP 8.0 или новее. Подключите пакет и Composer autoload:
composer config repositories.localzet-events vcs https://github.com/localzet/Eventscomposer require localzet/events:dev-mainВ примерах ниже используется localzet\Events. В долгоживущем worker подписки обычно регистрируют при запуске процесса. Каждый процесс имеет собственный набор подписчиков.
Отправка события
Заголовок раздела «Отправка события»use localzet\Events;
$id = Events::on('order.created', static function (array $order, string $event): string { return $event . ':' . $order['id'];});
$results = Events::emit('order.created', ['id' => 42]);echo $results[0]; // order.created:42
Events::off('order.created', $id);on() возвращает числовой идентификатор регистрации, а не события. Повторная регистрация того же callback создаёт ещё одну подписку. Сохраните ID, если обработчик понадобится удалить.
emit() передаёт каждому callback два аргумента: данные и фактическое имя события. Тип данных библиотека не ограничивает; договорённость о форме payload принадлежит приложению. Если callback требует другой тип, его TypeError будет обработан так же, как остальные исключения слушателей.
Подписка на семейство событий
Заголовок раздела «Подписка на семейство событий»$prefix = Events::on('order.*', static function (array $order, string $event): string { return 'audit:' . $event;});
$exact = Events::on('order.created', static fn (array $order): string => 'created');
$results = Events::emit('order.created', ['id' => 42]);// ['audit:order.created', 'created']Звёздочка работает только в конце имени и означает префикс строки. order.* совпадёт с order.created и order.; order* — ещё и с orders. Звёздочка в середине имени не является шаблоном. Несколько завершающих * удаляются при сохранении префикса; удобнее всегда использовать одну.
Точные и префиксные обработчики объединяются и сортируются по ID регистрации. Поэтому префиксный обработчик, зарегистрированный раньше точного, выполнится первым. Приоритета, связанного с длиной или точностью имени, нет.
Результаты и остановка
Заголовок раздела «Результаты и остановка»В обычном режиме результат emit() — массив значений callback в порядке вызова:
Events::on('permission', static fn () => null);Events::on('permission', static fn () => false);Events::on('permission', static fn () => 'not called');
$results = Events::emit('permission', null);// [null, false]Только строгое false останавливает распространение. Значения 0, '' и пустой массив не останавливают вызов следующих обработчиков. Побочные эффекты уже выполненных callback не откатываются.
Для поиска первого содержательного ответа передайте третий аргумент true:
$result = Events::emit('permission', null, true);// falseЭтот режим возвращает первый результат, отличный от null. Значение false тоже является ответом. Если слушателей нет или все вернули null, результат — null.
Исключения обработчиков
Заголовок раздела «Исключения обработчиков»emit() перехватывает любой Throwable внутри callback и продолжает выполнение. В обычном режиме объект исключения попадает в массив результатов:
foreach (Events::emit('order.created', ['id' => 42]) as $result) { if ($result instanceof Throwable) { error_log($result->getMessage()); }}Сам вызов emit() не выбросит наружу это исключение. В режиме поиска первого ответа исключение пропускается; если дальше нет результата, вернётся null. Для обязательной бизнес-операции, чей отказ должен прервать транзакцию, вызывайте сервис явно либо проверяйте результаты обычного режима.
Срок жизни подписки
Заголовок раздела «Срок жизни подписки»$id = Events::on('temporary.result', $listener);
try { Events::emit('temporary.result', $payload);} finally { Events::off('temporary.result', $id);}off() принимает то же имя регистрации, включая завершающую *. Удаление по фактическому имени события не найдёт префиксную подписку. Удалённый callback больше не удерживается реестром, но ссылки на него могут оставаться в коде приложения.
Перед началом emit() список слушателей копируется в локальную переменную. Подписка или отписка из callback влияет на следующие отправки; текущий список уже выбран. Повторный emit() из обработчика выполняется сразу, поэтому не создавайте цикл, в котором событие безусловно порождает само себя.
В публичном API нет сброса всех регистраций, одноразовой подписки или ограничения рекурсии. Временные подписки следует удалять по сохранённым ID.
Проверяемый пример
Заголовок раздела «Проверяемый пример»<?php
declare(strict_types=1);
use localzet\Events;
require __DIR__ . '/vendor/autoload.php';
$audit = Events::on('invoice.*', static fn (array $invoice, string $name): string => 'audit:' . $name);$paid = Events::on('invoice.paid', static fn (array $invoice): string => 'paid:' . $invoice['id']);
try { echo json_encode(Events::emit('invoice.paid', ['id' => 42]), JSON_THROW_ON_ERROR), PHP_EOL;} finally { Events::off('invoice.*', $audit); Events::off('invoice.paid', $paid);}Выбор способа взаимодействия
Заголовок раздела «Выбор способа взаимодействия»Events удобен для синхронных уведомлений внутри одного worker. Для сообщения другому worker нужен отдельный транспорт. Для выполнения после перезапуска, повторных попыток и гарантированного хранения нужна очередь с сохранением сообщений. Медленный callback задерживает отправителя и все последующие обработчики текущего события.
Параметры и возвращаемые значения API
Исходный код · AGPL-3.0