solidframe / cqrs
CQRS building blocks: Command, Query, Handlers, and Bus implementations for SolidFrame
Requires
- php: ^8.2
- solidframe/core: ^0.1
This package is auto-updated.
Last update: 2026-04-19 11:25:04 UTC
README
Command Query Responsibility Segregation: CommandBus, QueryBus, Handlers, and Middleware.
Commands produce side effects and return nothing. Queries return data and produce no side effects.
Installation
composer require solidframe/cqrs
Quick Start
Define a Command and Handler
use SolidFrame\Cqrs\Command; use SolidFrame\Cqrs\CommandHandler; final readonly class PlaceOrder implements Command { public function __construct( public string $orderId, public string $customerId, ) {} } final readonly class PlaceOrderHandler implements CommandHandler { public function __construct(private OrderRepository $orders) {} public function __invoke(PlaceOrder $command): void { $order = Order::place( new OrderId($command->orderId), new CustomerId($command->customerId), ); $this->orders->save($order); } }
Define a Query and Handler
use SolidFrame\Cqrs\Query; use SolidFrame\Cqrs\QueryHandler; final readonly class GetOrderById implements Query { public function __construct(public string $orderId) {} } final readonly class GetOrderByIdHandler implements QueryHandler { public function __construct(private OrderRepository $orders) {} public function __invoke(GetOrderById $query): ?OrderDto { $order = $this->orders->find(new OrderId($query->orderId)); return $order ? OrderDto::fromEntity($order) : null; } }
Dispatch
// Command — fire and forget $commandBus->dispatch(new PlaceOrder( orderId: 'order-123', customerId: 'customer-456', )); // Query — get result $order = $queryBus->ask(new GetOrderById(orderId: 'order-123'));
Standalone Usage
Without a framework bridge, wire the bus manually:
use SolidFrame\Cqrs\Handler\InMemoryHandlerResolver; use SolidFrame\Cqrs\Bus\CommandBus; use SolidFrame\Cqrs\Bus\QueryBus; $resolver = new InMemoryHandlerResolver(); $resolver->register(PlaceOrder::class, new PlaceOrderHandler($orders)); $commandBus = new CommandBus($resolver); $commandBus->dispatch(new PlaceOrder('order-123', 'customer-456'));
Middleware
Add cross-cutting concerns to bus processing.
use SolidFrame\Core\Middleware\MiddlewareInterface; final readonly class TransactionMiddleware implements MiddlewareInterface { public function __construct(private Connection $connection) {} public function handle(object $message, callable $next): mixed { $this->connection->beginTransaction(); try { $result = $next($message); $this->connection->commit(); return $result; } catch (\Throwable $e) { $this->connection->rollBack(); throw $e; } } } // With middleware $commandBus = new CommandBus($resolver, [ new TransactionMiddleware($connection), new LoggingMiddleware($logger), ]);
Middleware executes in registration order. Each middleware calls $next($message) to pass to the next one.
Handler Resolution
Handlers are resolved by the message type passed to __invoke(). The convention:
PlaceOrdercommand →PlaceOrderHandler::__invoke(PlaceOrder $command)GetOrderByIdquery →GetOrderByIdHandler::__invoke(GetOrderById $query)
One handler per command/query. Multiple handlers for the same message type will throw an exception.
API Reference
| Class / Interface | Purpose |
|---|---|
Command |
Marker interface for commands |
Query |
Marker interface for queries |
CommandHandler |
Marker interface for command handlers |
QueryHandler |
Marker interface for query handlers |
CommandBus |
Dispatches commands through middleware to handlers |
QueryBus |
Dispatches queries through middleware to handlers |
MessageBus |
Abstract base for both buses |
HandlerResolverInterface |
Contract for resolving handlers |
InMemoryHandlerResolver |
In-memory handler registry |
HandlerNotFoundException |
Thrown when no handler matches |
Related Packages
- solidframe/core — Bus interfaces, Middleware contract
- solidframe/ddd — Entities and aggregates your handlers operate on
- solidframe/event-driven — Dispatch domain events after commands
- solidframe/laravel — Auto-discovery, DI,
make:cqrs-command,make:query - solidframe/symfony — Compiler pass, DI, same generators
Contributing
This repository is a read-only split of the solidframe/solidframe monorepo, auto-synced on every push to main. Issues, pull requests, and discussions belong in the monorepo.