brammm / tactishun
A simple PHP 8 command bus implementation
Requires
- php: ^8.3
- psr/container: ^2.0
Requires (Dev)
- doctrine/coding-standard: ^13.0
- phpstan/phpstan: ^2.1
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12.2
This package is auto-updated.
Last update: 2025-10-01 02:25:46 UTC
README
A lightweight command bus implementation for PHP 8.3+ projects, designed to provide a straightforward approach to hexagonal architecture and CQRS patterns.
Features
- Simple command bus pattern implementation
- PSR-11 container integration
- Extensible middleware system
- Zero configuration required to get started
- Full PHPStan coverage
Installation
Install via Composer:
composer require brammm/tactishun
Quickstart
use Brammm\Tactishun\CommandBus; use Brammm\Tactishun\CommandHandler\CommandHandler; use Brammm\Tactishun\HandledBy; // 1. Create your command #[HandledBy(SendWelcomeMailHandler::class)] final readonly class SendWelcomeMail { public function __construct( public UserId $userId, ) {} } // 2. Implement the handler /** @implements CommandHandler<SendWelcomeMail> */ final class SendWelcomeMailHandler implements CommandHandler { public function handle(object $command): void { // Fetch user from $command->userId and send welcome email } } // 3. Set up the command bus $container = new Container(); // Any PSR-11 compatible container $commandBus = new CommandBus($container); // 4. Dispatch commands $commandBus->handle(new SendWelcomeMail($user->id));
Usage
Handling multiple commands with a single command handler
It's possible to have multiple commands handled by a single command handler. Simply
add the HandledBy
attribute with the same handler to multiple command classes.
For convenience, you can extend from the MultipleCommandsHandler (instead of using the
CommandHandler interface). It forwards the command to separate handleCommandName
methods.
use Brammm\Tactishun\CommandHandler\MultipleCommandsHandler; use Brammm\Tactishun\HandledBy; #[HandledBy(UserCommandHandler::class)] final readonly class RegisterUser { } #[HandledBy(UserCommandHandler::class)] final readonly class DeactivateUser { } /** @extends MultipleCommandsHandler<RegisterUser|DeactivateUser> */ final class UserCommandHandler extends MultipleCommandsHandler { public function handleRegisterUser(RegisterUser $command): void { } public function handleDeactivateUser(DeactivateUser $command): void { } }
Extending functionality through middleware
Extend functionality with middleware that wraps command execution:
final readonly class LoggingMiddleware implements Middleware { public function __construct( private LoggerInterface $logger ) {} public function process(object $command, CommandHandler $commandHandler): void { $commandName = get_class($command); $this->logger->info("Executing command: {$commandName}"); $startTime = microtime(true); $commandHandler->handle($command); $executionTime = microtime(true) - $startTime; $this->logger->info("Command {$commandName} completed in {$executionTime}ms"); } } // Register middleware $commandBus->add(new LoggingMiddleware($logger));
Middleware are added First In, First Out.
Through Middleware, it is possible to provide async queued functionality. No Middleware to facilitate this are shipped with this package at the moment.
Custom CommandHandlerResolver
s
By default, Tactishun uses the AttributeCommandHandlerResolver
to find handlers via the
HandledBy
attribute. This allows the zero-configuration setup. If you'd rather not use
that attribute or have a different solution in mind, you can provide a custom resolver as
the second parameter:
class ClassMapResolver implements CommandHandlerResolver { private array $handlerMap = [ SendWelcomeMail::class => SendWelcomeMailHandler::class, ProcessPayment::class => ProcessPaymentHandler::class, ]; public function resolve(object $command): string { $commandClass = get_class($command); return $this->handlerMap[$commandClass] ?? throw new HandlerNotFound($commandClass); } } $commandBus = new CommandBus($container, new ClassMapResolver());
Note that command handler resolvers must return a class-string for a command handler that the container implementation can then resolve to an instance of that handler.
Inspiration
This library draws inspiration from:
- Ross Tuck's work on league/tactician
- The Slim Framework middleware dispatcher pattern