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 Laravel Package

termyn/cqrs

A lightweight CQRS package for Laravel that separates writes from reads. Route commands and queries through dedicated buses to single-purpose handlers, keeping controllers thin and application logic organized, testable, and easy to extend.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin. This is a zero-star, zero-dependent package, so treat it as a small internal library you happen to have downloaded: read the source before trusting it, and pin the exact version in composer.json ("termyn/cqrs": "0.x.y") so a silent patch release can't change handler registration semantics under you.

composer require termyn/cqrs
composer show termyn/cqrs   # confirm the symfony/messenger constraint

The "4.3 and higher" floor in the description is a signal: check whether composer.json actually allows Symfony Messenger 5.x/6.x/7.x. If it caps at ^4.3|^5.0, you're two majors behind current Symfony Messenger and will hit deprecations. Laravel itself doesn't ship Messenger, so this package will pull it in as a dependency — you are running Symfony components inside Laravel's container.

Read in this order (5–15 minutes, the repo is small):

  1. src/ top level — the bus interfaces. Expect something like CommandBusInterface and QueryBusInterface wrapping Symfony\Component\Messenger\MessageBusInterface.
  2. The handler contract — is it an interface (__invoke(Command $c)), a #[AsMessageHandler]-style attribute, or a tag name?
  3. The service provider / bundle class — this is the file that tells you whether it was designed for Symfony's DI (in which case you'll be re-wiring it) or is container-agnostic.
  4. Middleware/ — what's already provided (transaction wrapping, validation) and what the ordering assumptions are.

First use case: a single command + handler, synchronous, one file each.

// app/Application/Command/RegisterUser.php
final class RegisterUser
{
    public function __construct(
        public readonly string $email,
        public readonly string $name,
    ) {}
}

// app/Application/Command/RegisterUserHandler.php
final class RegisterUserHandler
{
    public function __invoke(RegisterUser $command): void
    {
        User::create(['email' => $command->email, 'name' => $command->name]);
    }
}

Then register the bus in a service provider. If the package ships Symfony-style auto-wiring you'll find a CompilerPass you can't use in Laravel; the portable fallback is to build the Messenger bus yourself and let the package's bus class wrap it:

// app/Providers/CqrsServiceProvider.php
public function register(): void
{
    $this->app->singleton(CommandBusInterface::class, fn () => new CommandBus(
        new MessageBus([
            new HandleMessageMiddleware(new HandlersLocator([
                RegisterUser::class => [new RegisterUserHandler()],
            ])),
        ])
    ));
}

Dispatch it from a controller and you've proved the whole loop end to end:

public function store(StoreUserRequest $request, CommandBusInterface $bus)
{
    $bus->dispatch(new RegisterUser($request->email, $request->name));
    return response()->noContent(201);
}

Do the query side second, because that's where the interesting difference lies (see Gotchas — return values do not come back from dispatch() for free).

Implementation Patterns

Thin controller, fat handler. The controller's only job is HTTP: validate with a FormRequest, map to a command DTO, dispatch, map result to a response. Every branch in the handler is a branch you can unit test without Illuminate\Testing.

public function update(UpdateUserRequest $request, User $user, CommandBusInterface $bus): JsonResponse
{
    $bus->dispatch(new UpdateUser(
        id: $user->id,
        email: $request->validated('email'),
    ));

    return UserResource::make($user->refresh())->response();
}

One handler per message. Messenger's HandlersLocator is a message class => [handlers] map; multiple handlers per command mean all of them run and any exception aborts the rest. Keep the map 1:1 for commands and reserve fan-out for events (UserRegistered → send welcome mail, create trial subscription, write audit row). Events are the reason to use this package at all over a hand-rolled make(Handler::class)->handle($command).

Register handlers by reflection rather than by hand. The hand-written HandlersLocator above rots the moment someone adds a command. Scan your application namespace in the provider:

$handlers = collect(File::allFiles(app_path('Application')))
    ->filter(fn ($f) => str_ends_with($f->getFilename(), 'Handler.php'))
    ->map(fn ($f) => /* class name from path */)
    ->merge(...) // etc.

This is where the package either helps (a HandlerLocator you can feed Laravel's container) or doesn't (Symfony's ContainerInterface locator, which you bridge with fn ($class) => app($class)). Check src/ for a locator class before writing your own — the bridging one-liner is usually the whole value-add.

Middleware for cross-cutting concerns. This is the most leverage you get from Messenger. A transactional command middleware is ten lines and removes DB::transaction() from every handler:

final class TransactionMiddleware implements MiddlewareInterface
{
    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        return DB::transaction(fn () => $stack->next()->handle($envelope, $stack));
    }
}

Order matters and is left-to-right outermost-first: Validation → Transaction → HandleMessage. Put validation outermost so a malformed command never opens a transaction.

Keep commands and queries in separate namespaces and separate buses. app/Application/Command, app/Application/Query, app/Application/Event. Give the query bus a stricter middleware stack (no transaction, no retry) and make that asymmetry visible in the provider. It's the cheapest way to stop a colleague from writing to the DB in a query handler.

Route async work through Messenger's routing, not Laravel's queue. If you need background processing, configure a Messenger transport and run messenger:consume, or route to sync and dispatch a Laravel job from the handler. Mixing QUEUE_CONNECTION=redis with Messenger's transport config produces two competing workers and jobs that silently never run — the classic symptom is "it works in tests, nothing happens in prod."

Test at two levels. Unit-test the handler as a plain object (no bus, no container). Then test the wiring once per command:

public function test_controller_dispatches_register_user(): void
{
    $bus = Mockery::mock(CommandBusInterface::class);
    $bus->shouldReceive('dispatch')->once()
        ->with(Mockery::on(fn ($c) => $c instanceof RegisterUser && $c->email === '[email protected]'));
    $this->app->instance(CommandBusInterface::class, $bus);

    $this->postJson('/users', ['email' => '[email protected]', 'name' => 'A'])->assertCreated();
}

Mocking the interface (rather than Messenger's) is the reason the package's bus abstraction earns its keep — it's the seam that makes controllers testable without a real bus.

Map from FormRequest in one place. Either a static ::fromRequest() on the command or a small mapper class. Do not let $request leak into the handler; it's the single biggest source of "why is this command hard to test."

Gotchas and Tips

dispatch() returns an Envelope, not your result. This is the number-one trap for a Laravel developer arriving from $this->dispatchSync(). Symfony Messenger's contract is dispatch(object $message): Envelope; query handlers return values via a HandledStamp. Your query bus must unwrap:

$envelope = $this->bus->dispatch($query);
return $envelope->last(HandledStamp::class)?->getResult();

If the package's QueryBus doesn't do this, every query returns Envelope and you'll spend an hour on it. Verify with a one-line test before building anything on top.

Handler exceptions are wrapped in HandlerFailedException. Laravel's exception handler sees the wrapper, not your ValidationException or ModelNotFoundException, so $e->getMessage() is "Handling failed..." and instanceof checks fail. Either unwrap in the bus (throw $e->getPrevious() ?? $e) or register a renderer. Without this, abort(404) inside a handler becomes a 500.

No Laravel auto-discovery. Laravel won't find the package's service provider or handler tags. Everything that Symfony would do via a bundle CompilerPass you do manually in a register()/boot(). Budget an hour for the first wiring; it doesn't get easier with more commands, so write the reflection-based locator early.

Serialization only matters once you go async. Sync dispatch passes the object in memory and every DTO works. The moment the message hits a transport, Messenger's serializer round-trips it — readonly promoted properties survive, but closures, Eloquent models, and Illuminate\Support\Collection do not. Rule: commands carry scalars and IDs only, never models. This also fixes the "handler receives a detached model" confusion.

allow_no_handler is silent by default. If a handler isn't registered, Messenger's HandleMessageMiddleware throws NoHandlerForMessageException — unless the flag is set, in which case the command is dispatched and discarded with no error. Check the package's default. A typo'd class in your locator map produces a very quiet no-op.

Config caching bites reflection. If your handler locator uses File::allFiles() at boot, php artisan config:cache is fine but optimize in a container that lacks app_path('Application') (e.g., a slimmed production image) isn't. Prefer generating a static map at build time, or accept the scan and ensure the code tree ships.

Don't run long-lived Messenger workers against Laravel without restarts. messenger:consume has no --max-jobs; Eloquent's connection state and container singletons leak. Either implement a stop-after-N handler, use Supervisor with stopwaitsecs, or route async work back through queue:work with --max-jobs.

Version risk. With 0 stars and 0 dependents, assume no maintenance. Two concrete hedges: (1) write your own thin CommandBus/QueryBus classes implementing the same interfaces so you can delete the dependency without touching call sites, and (2) keep the package's surface confined to the service provider — if Handler, Command, or Middleware classes appear in your domain namespaces, you're coupled. The Envelope/HandledStamp/HandlerFailedException knowledge above is Messenger's, not the package's, and it's all you need to reimplement this in ~80 lines.

Extension points worth knowing: custom Stamp classes for correlation IDs and tenant context (attach before dispatch, read in middleware), a MessageLoggerMiddleware for a poor-man's audit log, and wrapIntoStamps() if you want to attach metadata to every command at the bus boundary rather than at each call site.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core