Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cqrs Handler Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

  1. composer require lmc/cqrs-handler.
  2. Read 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.
  3. Start with one read-side query returning a typed DTO:
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));
    }
}
  1. Register the map once, in a dedicated 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),
));
  1. Expose a three-line bus so nothing else in the app touches the resolver:
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.

Implementation Patterns

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.

Gotchas and Tips

  • Adoption risk is the headline gotcha. 1 star, 0 dependents, single-vendor maintenance. Pin the exact version in 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.
  • No auto-wiring. If you pass class-strings into 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."
  • Exact class match. The map is keyed by concrete class name; subclassing a command will not resolve to the parent's handler. Use ::class constants in the map, never string literals, so refactors don't orphan entries.
  • Interface signature mismatch fails late. A handler method that doesn't match the interface (wrong name, wrong return type) still gets returned by the resolver and only fatals when invoked. The per-handler smoke test above is cheap insurance.
  • Singletons and Octane. HandlerMap as a singleton is correct; stateful handlers are not. Never store request data on a handler property — Octane will leak it across requests.
  • Missing-handler exceptions surface as 500s. The resolver throws a package exception when no entry exists. Catch it at the bus boundary and rethrow something meaningful (or map it in bootstrap/app.php), otherwise a typo in the map looks like an application bug.
  • Config caching and closures. Class-strings in 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.
  • Don't use Eloquent models as messages. They serialise badly for queues, bloat payloads, and drag the ORM into your message contract. Use readonly scalar DTOs.
  • Observability is manual. Telescope won't record these dispatches. Log 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().
  • The handler-factory callable is the real extension point. It's where you decorate individual handlers (profiling, retries), lazy-load expensive handlers, and swap in fakes during tests. Treat it as part of your public API and document it.
  • Know when to skip it. Laravel's dispatcher with container-injected 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource