Дата обновления перевода 2021-09-28

Несколько автобусов

Распространенной архитектурой при создании приложений является разделение команд и запросов. Команды - это действия, которые что-то делают, а запросы - извлекают данные. Это называется CQRS (Разделение назначений запросов и команд). Прочтите статью о CQRS Мартина Фоулера, чтобы узнать больше. Эта архитектура может быть использована совместно с компонентом Messenger путем определения нескольких автобусов.

Автобус команд немного отличается от автобуса запросов. К примеру, автобусы команд обычно не предоставляют никаких результатов, а автобусы запросов редко бывают асинхронными. Вы можете сконфигурировать эти автобусы и их правила, используя промежуточное ПО.

Еще может быть хорошей идеей разделять действия от реакций, путем введения автобуса событий. Автобус событий может иметь 0 и более подписчиков.

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    framework:
        messenger:
            # Автобус, который будет внедрен при внедрении MessageBusInterface
            default_bus: command.bus
            buses:
                command.bus:
                    middleware:
                        - validation
                        - doctrine_transaction
                query.bus:
                    middleware:
                        - validation
                event.bus:
                    # промежуточное ПО 'allow_no_handlers' позволяет не иметь сконфигурированного
                    # обработчика для этого автобуса, не вызывая исключения
                    default_middleware: allow_no_handlers
                    middleware:
                        - validation
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <!-- config/packages/messenger.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <!-- Автобус, который будет внедрен при внедрении MessageBusInterface -->
            <framework:messenger default-bus="command.bus">
                <framework:bus name="command.bus">
                    <framework:middleware id="validation"/>
                    <framework:middleware id="doctrine_transaction"/>
                </framework:bus>
                <framework:bus name="query.bus">
                    <framework:middleware id="validation"/>
                </framework:bus>
                <!-- промежуточное ПО 'allow_no_handlers' позволяет не иметь сконфигурированного
                     обработчика для этого автобуса, не вызывая исключения -->
                <framework:bus name="event.bus" default-middleware="allow_no_handlers">
                    <framework:middleware id="validation"/>
                </framework:bus>
            </framework:messenger>
        </framework:config>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // config/packages/messenger.php
    use Symfony\Config\FrameworkConfig;
    
    return static function (FrameworkConfig $framework) {
        // Автобус, который будет внедрен при внедрении MessageBusInterface
        $framework->messenger()->defaultBus('command.bus');
    
        $commandBus = $framework->messenger()->bus('command.bus');
        $commandBus->middleware()->id('validation');
        $commandBus->middleware()->id('doctrine_transaction');
    
        $queryBus = $framework->messenger()->bus('query.bus');
        $queryBus->middleware()->id('validation');
    
        $eventBus = $framework->messenger()->bus('event.bus');
        // промежуточное ПО 'allow_no_handlers' позволяет не иметь сконфигурированного
        // обработчика для этого автобуса, не вызывая исключения
        $eventBus->defaultMiddleware('allow_no_handlers');
        $eventBus->middleware()->id('validation');
    };
    

Это создаст три новых сервиса:

  • command.bus: автомонтируемый с подсказкой MessageBusInterface (так как это default_bus);
  • query.bus: автомонтируемый с помощью MessageBusInterface $queryBus;
  • event.bus: автомонтируемый с помощью MessageBusInterface $eventBus.

Ограничение обработчиков по автобусам

По умолчанию, каждый обработчик будет доступен для обработки сообщений во всех ваших автобусах. Чтобы предовтратить запуск сообщения в неправильный автобус без ошибки, вы можете ограничить каждого обработчика по конкретному автобусу, используя тег messenger.message_handler:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/services.yaml
    services:
        App\MessageHandler\SomeCommandHandler:
            tags: [{ name: messenger.message_handler, bus: command.bus }]
            # предотвращает обработчиков от двойной регистрации (или вы можете удалить
            # MessageHandlerInterface, который автоконфигуратор использует для обнаружения обработчиков)
            autoconfigure: false
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="App\MessageHandler\SomeCommandHandler">
                <tag name="messenger.message_handler" bus="command.bus"/>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    // config/services.php
    $container->services()
        ->set(App\MessageHandler\SomeCommandHandler::class)
        ->tag('messenger.message_handler', ['bus' => 'command.bus']);
    

Таким образом, обработчик App\MessageHandler\SomeCommandHandler будет известен только автобусу command.bus.

Вы также можете автоматически добавить этот тег к определенному количеству классов, используя _экземпляр конфигурации сервиса. Используя это, вы сможете определить автобус сообщений, основываясь на реализованном интерфейсе:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    # config/services.yaml
    services:
        # ...
    
        _instanceof:
            # все сервисы, реализующие CommandHandlerInterface
            # будут зарегистрированы в автобусе command.bus
            App\MessageHandler\CommandHandlerInterface:
                tags:
                    - { name: messenger.message_handler, bus: command.bus }
    
            # в то время как те, что реализуют QueryHandlerInterface, будут
            # зарегистрированы в автобусе query.bus
            App\MessageHandler\QueryHandlerInterface:
                tags:
                    - { name: messenger.message_handler, bus: query.bus }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <!-- все сервисы, реализующие CommandHandlerInterface
                 будут зарегистрированы в автобусе command.bus -->
            <instanceof id="App\MessageHandler\CommandHandlerInterface">
                <tag name="messenger.message_handler" bus="command.bus"/>
            </instanceof>
    
            <!-- в то время как те, что реализуют QueryHandlerInterface, будут
                 зарегистрированы в автобусе query.bus -->
            <instanceof id="App\MessageHandler\QueryHandlerInterface">
                <tag name="messenger.message_handler" bus="query.bus"/>
            </instanceof>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\MessageHandler\CommandHandlerInterface;
    use App\MessageHandler\QueryHandlerInterface;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        // ...
    
        // все сервисы, реализующие CommandHandlerInterface
        // будут зарегистрированы в автобусе command.bus
        $services->instanceof(CommandHandlerInterface::class)
            ->tag('messenger.message_handler', ['bus' => 'command.bus']);
    
        // в то время как те, что реализуют QueryHandlerInterface, будут
        // зарегистрированы в автобусе query.bus
        $services->instanceof(QueryHandlerInterface::class)
            ->tag('messenger.message_handler', ['bus' => 'query.bus']);
    };
    

Отладка автобусов

Команда debug:messenger перечисляет доступные сообщения и обработчики для каждого автобуса. Вы также можете ограничить список по определенному автобусу, предоставив его название в качестве аргумента.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ php bin/console debug:messenger

  Messenger
  =========

  command.bus
  -----------

   Следующие сообщения могут быть запущены:

   ---------------------------------------------------------------------------------------
    App\Message\DummyCommand
        handled by App\MessageHandler\DummyCommandHandler
    App\Message\MultipleBusesMessage
        handled by App\MessageHandler\MultipleBusesMessageHandler
   ---------------------------------------------------------------------------------------

  query.bus
  ---------

   Следующие сообщения могут быть запущены:

   ---------------------------------------------------------------------------------------
    App\Message\DummyQuery
        handled by App\MessageHandler\DummyQueryHandler
    App\Message\MultipleBusesMessage
        handled by App\MessageHandler\MultipleBusesMessageHandler
   ---------------------------------------------------------------------------------------

Tip

Начиная с Symfony 5.1, команда также будет отображать описание сообщения PHPDoc и классы обработчика.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.