lmc/cqrs-handler
Laravel CQRS handler package that simplifies command and query buses with automatic handler resolution, dependency injection, and clean separation of write and read logic. Ideal for structuring growing applications with maintainable, testable business actions.
lmc/cqrs-handler is a thin, framework-agnostic abstraction: marker interfaces for commands, queries and their handlers, plus a HandlerMap (message class → handler) and a HandlerResolver that turns a message into a handler. It is not a bus, not a container integration, and it ships no Laravel service provider — you wire it yourself, and that wiring is the whole "getting started" story.
composer require lmc/cqrs-handler.vendor/lmc/cqrs-handler/src/Handler/ first. HandlerMap and HandlerResolver are the only classes with behaviour you configure; Command, Query, Type and the handler interfaces are contracts. Confirm two names before you write code: the resolver's lookup method and whether handlers are called via __invoke() or handle(). That five-minute check prevents the only real first-install error.namespace App\Cqrs\Query;
use Lmc\Cqrs\Handler\Query\Query;
final class FindOrder implements Query
{
public function __construct(public readonly string $orderId) {}
}
namespace App\Cqrs\Query;
use Lmc\Cqrs\Handler\Query\QueryHandler;
final class FindOrderHandler implements QueryHandler
{
public function __construct(private OrderRepository $orders) {}
public function __invoke(FindOrder $query): OrderDetail
{
return OrderDetail::fromModel($this->orders->findOrFail($query->orderId));
}
}
CqrsServiceProvider:$this->app->singleton(HandlerMap::class, fn () => new HandlerMap([
FindOrder::class => FindOrderHandler::class,
]));
$this->app->bind(HandlerResolver::class, fn ($app) => new HandlerResolver(
$app->make(HandlerMap::class),
fn (string $handler) => $app->make($handler),
));
final class QueryBus
{
public function __construct(private HandlerResolver $resolver) {}
public function ask(Query $query): mixed
{
$handler = $this->resolver->resolve($query); // confirm method name
return $handler($query);
}
}
That is the entire package. Everything after this is your architecture, not the library's.
One bus per side, injected as an interface. Type-hint QueryBus/CommandBus (your own interfaces), never HandlerResolver, in controllers and jobs. It gives you a single seam for decorators later and keeps the vendor class out of your domain.
Exploit the class-keyed map. Because the map is keyed by message class, several messages can point at one handler. This is exactly how LMC's own HTTP packages route many request messages into a single send/fetch handler. Use it for families of messages (e.g. SendSms, SendEmail → SendMessageHandler) instead of creating a handler per message.
Decorators for cross-cutting concerns. Wrap the bus, not the handlers: DB::transaction() around command dispatch, timing/metrics, structured logging of message::class → handler::class, and validation of commands that arrive from queue workers or Artisan (there is no FormRequest there).
final class TransactionalCommandBus implements CommandBus
{
public function __construct(private CommandBus $inner) {}
public function dispatch(Command $command): mixed
{
return DB::transaction(fn () => $this->inner->dispatch($command));
}
}
Keep the CQRS split honest. Commands return void or an ID; queries return a DTO and never touch writes. If you use a read replica, bind a read-only connection inside QueryBus — this is the one place the split pays for itself.
Typed returns, always. Have ask() return a Response/Type contract (or your own QueryResult) rather than mixed. Handlers returning arrays work fine but silently erode the contract, and the HTTP layer stops being a one-liner.
Queued commands. The package is synchronous. Let the command implement ShouldQueue, dispatch a job whose handle(CommandBus $bus) calls $bus->dispatch($this->command), and keep handlers container-resolvable so the worker can rebuild them.
Config-driven map. Put entries in config/cqrs.php as class-string arrays and compose the HandlerMap from it. You get config:cache for free and a single place to grep when "which handler runs for this?" comes up. Never put closures in that config — config:cache will fail.
Test at two levels. Unit-test handlers directly with a fake repository (new FindOrderHandler($fake)), and add one integration test that asserts every mapped handler is constructible — this catches the missing-dependency failure at CI time rather than in production:
public function testEveryMappedHandlerIsResolvable(): void
{
$map = $this->app->make(HandlerMap::class);
foreach ($map->getHandlers() as $message => $handler) { // adapt to actual accessor
$this->assertInstanceOf($handler, $this->app->make($handler), $message);
}
}
Compose maps per module. If you have bounded contexts, build a HandlerMap per context and merge them in the provider. It keeps the map readable and lets a module be extracted later.
composer.json and keep the vendor surface to the two classes you actually use (HandlerMap, HandlerResolver) plus the marker interfaces. Swapping to Laravel's Illuminate\Contracts\Bus\Dispatcher should be an afternoon, not a rewrite — design for that.HandlerMap but your resolver does new $class, constructor injection silently breaks. Always resolve handlers through the container via the handler-factory callable. This is the #1 way people conclude "the package is broken."::class constants in the map, never string literals, so refactors don't orphan entries.HandlerMap as a singleton is correct; stateful handlers are not. Never store request data on a handler property — Octane will leak it across requests.bootstrap/app.php), otherwise a typo in the map looks like an application bug.config/cqrs.php are cache-safe; closures or anonymous classes are not. Run php artisan optimize:clear in deploys if you cache the map outside config.readonly scalar DTOs.message::class → handler::class at debug level in the bus decorator, or fire your own CommandHandled event; without it, tracing a mis-routed message means dd().handle() methods and ShouldQueue covers most of what this package offers. Choose lmc/cqrs-handler when you specifically want an explicit, inspectable message→handler map that is framework-independent and shareable with non-Laravel entry points (CLI, Symfony, workers) — otherwise the extra indirection is cost without benefit.How can I help you explore Laravel packages today?