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

Подключить контейнер

Слушателю почти всегда что-то нужно: почтовый сервис, репозиторий, HTTP-клиент. Создавать это руками в каждом слушателе — путь к дублированию и хаосу. Для этого есть контейнер.

Без своего контейнера

Ничего делать не нужно. Роутер сам поднимает PHP-DI с автосвязыванием: классы с зависимостями в конструкторе создаются без регистрации.

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

final class Clock
{
    public function now(): string
    {
        return '14:03';
    }
}

final class Mailer
{
    public function __construct(private readonly Clock $clock) {}

    public function send(string $text): void
    {
        echo '[', $this->clock->now(), '] письмо: ', $text, PHP_EOL;
    }
}

// Слушатель статичный: зависимости берёт из контейнера сам
final class SendConfirmationEmail implements ListenerInterface
{
    public static function handle(EventInterface $event): void
    {
        Container::get(Mailer::class)->send('заказ ' . $event->getAttribute('order_id') . ' оформлен');
    }
}

// Middleware получает зависимости через конструктор: контейнер создаст его сам
final class StampTime implements MiddlewareInterface
{
    public function __construct(private readonly Clock $clock) {}

    public function process(EventInterface $event, Closure $next): void
    {
        $next($event->withAttribute('received_at', $this->clock->now()));
    }
}
use Selyusize\EventsRouter\Event;
use Selyusize\EventsRouter\EventRouterFactory;

$events = EventRouterFactory::create();   // без контейнера проекта — PHP-DI с автосвязыванием

$events->listen('shop.order.{order_id}.created', SendConfirmationEmail::class)
    ->add(StampTime::class);              // Clock в конструктор подставит контейнер

$events->dispatch(new Event('shop.order.42.created'));
[14:03] письмо: заказ 42 оформлен

Здесь работают два механизма:

  • Слушатель статичный, поэтому зависимости он берёт сам: Container::get(Mailer::class). Container — статический фасад библиотеки, Selyusize\EventsRouter\Container\Container.
  • Middleware — обычный объект. Достаточно указать имя класса в add(), а контейнер создаст его со всеми зависимостями в момент, когда событие дойдёт до этого middleware.

С контейнером проекта

Если в проекте уже есть контейнер, передайте его в фабрику:

$events = EventRouterFactory::create($container);   // любой Psr\Container\ContainerInterface

С этого момента и middleware, и Container::get() в слушателях работают с вашим контейнером. Два контейнера не появятся.

В проекте на стартовой архитектуре с Rasa\Container\Container это выглядит так:

use Rasa\Container\Container;

$events = EventRouterFactory::create(Container::getInstance());

Теперь Rasa\Container\Container::get() и Selyusize\EventsRouter\Container\Container::get() смотрят в один и тот же объект. В слушателях можно писать так, как принято в проекте.

Другой контейнер

Роутеру подходит любой контейнер, реализующий PSR-11 Psr\Container\ContainerInterface. PHP-DI нужен библиотеке только как запасной вариант, когда своего контейнера нет. От контейнера роутер ждёт одного: get(ИмяКласса::class) возвращает объект этого класса.

Контейнер Что нужно сделать
PHP-DI (как в стартовой архитектуре) ничего: создаёт любой класс сам
Laravel (Illuminate\Container\Container) ничего: тоже создаёт классы сам
Symfony DependencyInjection middleware и сервисы, которые слушатели берут через Container::get(), должны быть публичными сервисами с id, равным имени класса: по умолчанию сервисы Symfony приватные
простой контейнер «ключ → объект» зарегистрировать каждый middleware, который указан в маршрутах именем класса

Фасад Container — один на процесс. Если создать два роутера с разными контейнерами, Container::get() будет смотреть в последний переданный. В обычном приложении роутер один, и это не проблема.

Если контейнер не создаёт классы сам

PHP-DI создаёт любой класс без регистрации. Другие контейнеры так умеют не всегда. Если ваш контейнер работает только с тем, что в нём зарегистрировано, зарегистрируйте в нём и middleware, которые указаны в маршрутах именем класса.

Иначе контейнер бросит исключение, когда событие дойдёт до такого middleware. Слушатель за ним получит статус Failed, остальные отработают как обычно.

Если класса нет вовсе

Опечатку в имени класса роутер ловит сразу, ещё в listen() и add(), а не когда придёт событие. Исключение — InvalidRoute, в стеке вызовов видна строка файла маршрутов с ошибкой.