Перейти к содержанию

Передать данные объектом

Массив в payload удобен ровно до тех пор, пока не нужно вспомнить, какие в нём ключи. Через месяц $payload['amount'] превращается в угадайку: есть ли там currency, это рубли или копейки, а order_id строкой или числом?

Решение — DTO с именованным конструктором, который сам собирает имя события:

use Selyusize\EventsRouter\Event;

final class OrderPaid
{
    public function __construct(
        public readonly int $orderId,
        public readonly int $amount,
    ) {}

    /**
     * @return Event<self>
     */
    public static function event(int $orderId, int $amount): Event
    {
        return new Event('shop.order.' . $orderId . '.paid', new self($orderId, $amount));
    }
}

Имя события пишется один раз, рядом с данными, а не в каждом вызове dispatch():

use Selyusize\EventsRouter\EventRouterFactory;

$events = EventRouterFactory::create();
$events->listen('shop.order.{order_id}.paid', SendReceipt::class);

$events->dispatch(OrderPaid::event(42, 1500));

Слушатель получает тот же объект через getPayload():

use Selyusize\EventsRouter\Contract\Core\EventInterface;
use Selyusize\EventsRouter\Contract\Core\ListenerInterface;

final class SendReceipt implements ListenerInterface
{
    public static function handle(EventInterface $event): void
    {
        $order = $event->getPayload();
        assert($order instanceof OrderPaid);

        echo 'Чек по заказу ', $order->orderId, ' на ', $order->amount, ' ₽', PHP_EOL;
    }
}
Чек по заказу 42 на 1500 ₽

Что это даёт:

  • в месте рассылки видно, какое событие уйдёт;
  • поля и их типы описаны в одном классе, IDE и Psalm их знают;
  • EventInterface объявлен с шаблоном @template TPayload, поэтому у Event<OrderPaid> статический анализ знает тип payload.

Почему библиотека не собирает имя события из класса сама — в Как это устроено.

Если DTO реализует PSR-14 StoppableEventInterface, слушатель может остановить рассылку — см. Обработать ошибки.