black/cqrs-php
Minimal CQRS command bus for PHP/DDD without event sourcing. Define Command and CommandHandler, register handlers to commands, then dispatch via a single Bus. Includes optional Symfony bundle integration with service tags for handler registration.
Installation:
composer require black/cqrs-php
"black/cqrs-php": "1.0.0") over @stable to avoid surprises.Basic Command/Handler:
// Command
class CreateUserCommand implements \Black\DDD\CQRSinPHP\Infrastructure\CQRS\Command {
public $name;
public $email;
public function __construct(string $name, string $email) {
$this->name = $name;
$this->email = $email;
}
}
// Handler
class CreateUserHandler implements \Black\DDD\CQRSinPHP\Infrastructure\CQRS\CommandHandler {
public function handle(CreateUserCommand $command) {
// Business logic here
return new User($command->name, $command->email);
}
}
Register and Dispatch:
$bus = new \Black\DDD\CQRSinPHP\Infrastructure\CQRS\Bus;
$bus->register(CreateUserCommand::class, new CreateUserHandler());
$bus->handle(new CreateUserCommand('John', 'john@example.com'));
CreateUserCommand with required fields.CreateUserHandler to process the command (e.g., save to DB).Command Design:
class UpdateProductStockCommand implements Command {
public $productId;
public $quantity;
public function __construct(int $productId, int $quantity) {
$this->productId = $productId;
$this->quantity = $quantity;
}
}
Handler Registration:
$bus->register(UpdateProductStockCommand::class, new UpdateStockHandler());
services:
App\Handler\UpdateStockHandler:
tags:
- { name: black_cqrs.handler, command: "App\Command\UpdateProductStockCommand" }
Dependency Injection:
public function __construct(private Bus $bus) {}
public function execute(CreateOrderCommand $command) {
$this->bus->handle($command);
}
Error Handling:
handle() calls in try-catch:
try {
$bus->handle($command);
} catch (CommandException $e) {
// Log or rethrow
}
Laravel Integration:
AppServiceProvider:
$this->app->singleton(Bus::class, function () {
$bus = new Bus();
// Register handlers manually or via tags (if using SymfonyBundle)
return $bus;
});
use App\Facades\CQRS;
CQRS::handle(new CreateUserCommand(...));
Testing:
$bus = $this->createMock(Bus::class);
$bus->expects($this->once())->method('handle');
$handler = new CreateUserHandler($bus);
Logging:
class LoggingBus implements Bus {
public function handle(Command $command) {
\Log::info("Handling command: " . get_class($command));
$this->bus->handle($command);
}
}
Handler Registration:
CommandNotRegisteredException.black_cqrs.handler tag or manually register in a service provider.Command Immutability:
private and only accessible via constructor.Circular Dependencies:
SymfonyBundle Limitation:
Command Not Found:
App\Command\CreateUserCommand vs. App\Command\CreateUserCommand).Handler Not Invoked:
CommandHandler and is properly registered.dd($bus->getHandlers()) to inspect registered handlers.Bus Singleton:
No Event Sourcing:
spatie/laravel-event-sourcing.Custom Bus:
Bus to add middleware (e.g., logging, validation):
class MiddlewareBus implements Bus {
public function handle(Command $command) {
$this->validate($command);
$this->log($command);
$this->bus->handle($command);
}
}
Command Validation:
class ValidateCommandMiddleware {
public function __invoke(Command $command, callable $next) {
if (!$command instanceof ValidatableCommand) return $next($command);
if (!$command->isValid()) {
throw new InvalidCommandException();
}
return $next($command);
}
}
Async Handling:
$bus->handle(new SendEmailCommand(...)); // Sync
dispatch(new HandleCommandJob($command)); // Async
How can I help you explore Laravel packages today?