ICanBoogie/MessageBus v3.0 provides an implementation of the message bus pattern, with support for permissions and voters. It comes with a simplified API, as the separation between "query" and "command" is gone.

Recap on the Message Bus

A message bus helps to separate presentation concerns from business logic by mapping inputs of various sources to simpler application messages. It also helps to decouple the domain from the implementation, for an application only has to know about the messages, not how they are handled. A design well known in Hexagonal architecture.

Permissions and voters

One of the reasons to have distinct dispatchers for queries and commands was to decorate the command dispatcher with restrictions, using a AssertingDispatcher for instance. We had a situation where controllers needed to request both dispatchers in order to be able to dispatch queries and commands. Besides, in some situation, queries were also subject to restrictions. To simplify the usage and make restrictions more practicals, permissions and voters have been added.

Three tags are used to define voters, message handlers, and permissions (they are configurable):

  • message_bus.voter: tags a voter service. The permission attribute specifies the identifier of that permissions e.g. can_manage_menu.

  • message_bus.handler: tags a message handler service. The message attribute specifies the message class handled by the service.

  • message_bus.permission: tags message handlers that require permission check. The permission attribute specifies the permission to check. Use the tag multiple times for multiple permission checks.

You probably want to restrict the dispatch of messages to certain conditions. For example, deleting records should only be possible for users having a certain scope in their JWT. For this, you want to make sure of a few things:

  1. Define voters and the permission they vote for.

    services:
      Acme\MenuService\Presentation\Security\Voters\CanManageMenu:
        tags:
        - { name: message_bus.voter, permission: can_manage_menu }
    
      Acme\MenuService\Presentation\Security\Voters\CanDeleteMenu:
        tags:
        - { name: message_bus.voter, permission: can_delete_menu }
  2. Tag the permissions together with the handler and message definition.

    services:
      Acme\MenuService\Application\MessageBus\DeleteMenuHandler:
        tags:
        - { name: message_bus.handler, message: Acme\MenuService\Application\MessageBus\DeleteMenu }
        - { name: message_bus.permission, permission: can_manage_menu }
        - { name: message_bus.permission, permission: can_delete_menu }
  3. Require a RestrictedDispatcher instead of a Dispatcher.

    <?php
    
    // ...
    
    use ICanBoogie\MessageBus\RestrictedDispatcher;
    
    final class MenuController
    {
        public function __construct(
            private RestrictedDispatcher $dispatcher
        ) {}
    }
  4. Put in the context whatever is required for the voters to make their decision. In the following example, that would be a token, for the voter to check for scopes.

    <?php
    
    // ...
    
    use ICanBoogie\MessageBus\Context;
    
    final class MenuController
    {
        // ...
    
        public function delete(Request $request): Response
        {
            // ...
    
            $this->dispatch(
                new MenuDelete($id),
                new Context([ $token ])
            );
        }
    }