Defining a command and its handler
A command is any class implementing Command\CommandInterface:
namespace App\Command;
use Webware\MessageBus\Command\CommandInterface;
final readonly class CreateUser implements CommandInterface
{
public function __construct(
public string $email,
) {}
}
CommandHandlerInterface is a marker. It declares no method, so the handler declares the method its
wired strategy expects. Under the default HandleStrategy that method is handle():
namespace App\Command;
use Psl\Type;
use Webware\MessageBus\Command\CommandInterface;
use Webware\MessageBus\Command\CommandResult;
use Webware\MessageBus\CommandHandlerInterface;
use Webware\MessageBus\MessageInterface;
use Webware\MessageBus\MessageStatus;
use Webware\MessageBus\ResultInterface;
final readonly class CreateUserHandler implements CommandHandlerInterface
{
public function handle(MessageInterface $message): ResultInterface
{
// narrow from the broad message type to this specific command
$command = Type\instance_of(CreateUser::class)->assert($message);
// ...create the user...
$userId = 42;
return new CommandResult($command, MessageStatus::Success, $userId);
}
}
There is no #[Override] on handle(): the marker declares nothing to override. The method parameter
is yours to choose. Declaring it as MessageInterface|CommandInterface is runtime-safe (the union is
at least as wide as MessageInterface) and tells static analysis that this handler only receives
commands, without narrowing too early. You still need Type\instance_of() (or an instanceof check)
to narrow to CreateUser itself.
Named-method handlers
Opt into ClassnameStrategy and the handler declares a method named after the message's short class
name (CreateUser -> createUser), typed directly to the concrete class:
namespace App\Command;
use Webware\MessageBus\Command\CommandResult;
use Webware\MessageBus\CommandHandlerInterface;
use Webware\MessageBus\MessageStatus;
final readonly class CreateUserHandler implements CommandHandlerInterface
{
public function createUser(CreateUser $message): CommandResult
{
// ...create the user...
$userId = 42;
return new CommandResult($message, MessageStatus::Success, $userId);
}
}
No handle(), no narrowing assertion. See Strategy for how to wire
the strategy.
Mapping the command to its handler
Extend the command_map in your application config and register the handler with your container:
use Webware\MessageBus\ConfigProvider;
use Webware\MessageBus\MessageBusInterface;
return [
'dependencies' => [
'factories' => [
App\Command\CreateUserHandler::class => App\Command\CreateUserHandlerFactory::class,
],
],
MessageBusInterface::class => [
ConfigProvider::COMMAND_MAP_KEY => [
App\Command\CreateUser::class => App\Command\CreateUserHandler::class,
],
],
];
Queries follow the exact same shape under ConfigProvider::QUERY_MAP_KEY, using
Query\QueryInterface/QueryHandlerInterface and Query\QueryResult instead.
To use ClassnameStrategy, add one alias:
use Webware\MessageBus\Strategy\ClassnameStrategy;
use Webware\MessageBus\StrategyInterface;
return [
'dependencies' => [
'aliases' => [
StrategyInterface::class => ClassnameStrategy::class,
],
],
];
Dispatching
/** @var Psr\Container\ContainerInterface $container */
$bus = $container->get(MessageBusInterface::class);
$result = $bus->handle(new App\Command\CreateUser('jane@example.com'));
$result->getStatus(); // MessageStatus::Success or ::Failure
$result->getResult(); // 42, in this example
Writing custom middleware
Middleware implements MiddlewareInterface and decides whether/when to call the next handler in the
pipeline:
namespace App\Middleware;
use Override;
use Webware\MessageBus\MessageInterface;
use Webware\MessageBus\MiddlewareInterface;
use Webware\MessageBus\PipelineHandlerInterface;
use Webware\MessageBus\ResultInterface;
final readonly class LoggingMiddleware implements MiddlewareInterface
{
public function __construct(
private App\Logging\LoggerInterface $logger,
) {}
#[Override]
public function process(
MessageInterface $message,
PipelineHandlerInterface $next,
): ResultInterface {
$this->logger->info('Dispatching ' . $message::class);
return $next->handle($message);
}
}
Register it as a service and add it to middleware_pipeline with a priority. Higher priority runs
earlier; Middleware\MessageHandlerMiddleware is registered at priority 1
(ConfigProvider::DEFAULT_PRIORITY) so it runs last by default. Give your middleware a higher priority
to run before the handler is resolved:
use Webware\MessageBus\ConfigProvider;
use Webware\MessageBus\MessageBusInterface;
return [
'dependencies' => [
'factories' => [
App\Middleware\LoggingMiddleware::class => App\Middleware\LoggingMiddlewareFactory::class,
],
],
MessageBusInterface::class => [
ConfigProvider::MIDDLEWARE_PIPELINE_KEY => [
[
'middleware' => App\Middleware\LoggingMiddleware::class,
'priority' => 10,
],
],
],
];
Middleware registered without a priority defaults to 1; entries sharing the same priority run in
the order they were declared. Middleware class-strings that aren't registered in the container are
silently skipped when the pipeline is built (see Configuration).
See Execution Flow for a diagram of how this
LoggingMiddleware example fits into the pipeline, and why it never sees the CommandResult.