Implementing a command bus¶
The classes and interfaces from this package can be used to set up a command bus. The characteristics of a command bus are:
- It handles commands, i.e. imperative messages
- Commands are handled by exactly one command handler
- The behavior of the command bus is extensible: middlewares are allowed to do things before or after handling a command
Setting up the command bus¶
At least we need an instance of MessageBusSupportingMiddleware
:
1 2 3 | use SimpleBus\Message\Bus\Middleware\MessageBusSupportingMiddleware;
$commandBus = new MessageBusSupportingMiddleware();
|
Finish handling a command, before handling the next¶
We want to make sure that commands are always fully handled before other commands will be handled, so we add a specialized middleware for that:
1 2 3 | use SimpleBus\Message\Bus\Middleware\FinishesHandlingMessageBeforeHandlingNext;
$commandBus->appendMiddleware(new FinishesHandlingMessageBeforeHandlingNext());
|
Defining the command handler map¶
Now we also want commands to be handled by exactly one command handler (which can be any callable). We first need to define the collection of handlers that are available in the application. We should make this command handler map lazy-loading, or every command handler will be fully loaded, even though it is not going to be used:
1 2 3 4 5 6 7 | use SimpleBus\Message\CallableResolver\CallableMap;
use SimpleBus\Message\CallableResolver\ServiceLocatorAwareCallableResolver;
// Provide a map of command names to callables. You can provide actual callables, or lazy-loading ones.
$commandHandlersByCommandName = [
'Fully\Qualified\Class\Name\Of\Command' => ... // a "callable"
];
|
Each of the provided “callables” can be one of the following things:
- An actual PHP callable,
- A service id (string) which the service locator (see below) can resolve to a PHP callable,
- An array of which the first value is a service id (string), which the service locator can resolve to a regular object, and the second value is a method name.
For backwards compatibility an object with a handle()
method also
counts as a “callable”.
1 2 3 4 5 6 7 8 9 10 11 | // Provide a service locator callable. It will be used to instantiate a handler service whenever requested.
$serviceLocator = function ($serviceId) {
$handler = ...;
return $handler;
}
$commandHandlerMap = new CallableMap(
$commandHandlersByCommandName,
new ServiceLocatorAwareCallableResolver($serviceLocator)
);
|
Resolving the command handler for a command¶
The name of a command¶
First we need a way to resolve the name of a command. You can use the fully-qualified class name (FQCN) of a command object as its name:
1 2 3 | use SimpleBus\Message\Name\ClassBasedNameResolver;
$commandNameResolver = new ClassBasedNameResolver();
|
Or you can ask command objects what their name is:
1 2 3 | use SimpleBus\Message\Name\NamedMessageNameResolver;
$commandNameResolver = new NamedMessageNameResolver();
|
In that case your commands have to implement NamedMessage
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | use SimpleBus\Message\Name\NamedMessage;
class YourCommand implements NamedMessage
{
public static function messageName()
{
return 'your_command';
}
}
.. rubric:: Implementing your own ``MessageNameResolver``
:name: implementing-your-own-messagenameresolver
If you want to use another rule to determine the name of a command,
create a class that implements
``SimpleBus\Message\Name\MessageNameResolver``.
|
Resolving the command handler based on the name of the command¶
Using the MessageNameResolver
of your choice, you can now let the
command handler resolver find the right command handler for a given
command.
1 2 3 4 5 6 | use SimpleBus\Message\Handler\Resolver\NameBasedMessageHandlerResolver;
$commandHandlerResolver = new NameBasedMessageHandlerResolver(
$commandNameResolver,
$commandHandlerMap
);
|
Finally, we should add some middleware to the command bus that calls the resolved command handler:
1 2 3 4 5 6 7 | use SimpleBus\Message\Handler\DelegatesToMessageHandlerMiddleware;
$commandBus->appendMiddleware(
new DelegatesToMessageHandlerMiddleware(
$commandHandlerResolver
)
);
|
Using the command bus: an example¶
Consider the following command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class RegisterUser
{
private $emailAddress;
private $plainTextPassword;
public function __construct($emailAddress, $plainTextPassword)
{
$this->emailAddress = $emailAddress;
$this->plainTextPassword = $plainTextPassword;
}
public function emailAddress()
{
return $this->emailAddress;
}
public function plainTextPassword()
{
return $this->plainTextPassword;
}
}
|
This command communicates the intention to “register a new user”. The message data consists of an email address and a password in plain text. This information is required to execute the desired behavior.
The handler for this command looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class RegisterUserCommandHandler
{
...
public function handle(RegisterUser $command)
{
$user = User::register(
$command->emailAddress(),
$command->plainTextPassword()
);
$this->userRepository->add($user);
}
}
|
We should register this handler as a service and add the service id to
the command handler map. Since we have
already fully configured the command bus, we can just start creating a
new command object and let the command bus handle it. Eventually the
command will be passed as a message to the
RegisterUserCommandHandler
:
1 2 3 4 5 6 7 8 9 | $command = new RegisterUser(
'matthiasnoback@gmail.com',
's3cr3t'
);
$commandBus->handle($command);
.. rubric:: Implementing your own command bus middleware
:name: implementing-your-own-command-bus-middleware
|
It’s very easy to extend the behavior of the command bus. You can
create a class that implements MessageBusMiddleware
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | use SimpleBus\Message\Bus\Middleware\MessageBusMiddleware;
/**
* Marker interface for commands that should be handled asynchronously
*/
interface IsHandledAsynchronously
{
}
class HandleCommandsAsynchronously implements MessageBusMiddleware
{
...
public function handle($message, callable $next)
{
if ($message instanceof IsHandledAsynchronously) {
// handle the message asynchronously using a message queue
$this->messageQueue->add($message);
} else {
// handle the message synchronously, i.e. right-away
$next($message);
}
}
}
|
You should add an instance of that class as middleware to any
MessageBusSupportingMiddleware
instance (like the command bus we
created earlier):
1 | $commandBus->appendMiddleware(new HandleCommandsAsynchronously());
|
Make sure that you do this at the right place, before or after you add the other middlewares.
Calling $next($message)
will make sure that the next middleware
in line is able to handle the message.
Logging messages
To log every message that passes through the command bus, add the
LoggingMiddleware
right before the
DelegatesToMessageHandlerMiddleware
. Make sure to set up a
PSR-3 compliant logger
first:
1 2 3 4 5 6 7 | use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
// $logger is an instance of LoggerInterface
$logger = ...;
$loggingMiddleware = new LoggingMiddleware($logger, LogLevel::DEBUG);
$commandBus->appendMiddleware($loggingMiddleware);
|
Continue to read about the perfect complement to the command bus: the event bus.