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

lmc/cqrs-bundle

A PHP CQRS bundle that separates write commands from read queries, giving you clean command and query buses with handlers, middleware support, and a structured way to organize business logic in your application.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and register manually.

    composer require lmc/cqrs-bundle
    

    There is no Flex recipe, so add the bundle class from vendor/lmc/cqrs-bundle/src/ to config/bundles.php yourself. Check the package's composer.json for the PHP/Symfony constraints — the last tagged release is from May 2022, so on a modern Symfony (6.4/7.x) you may need to allow the constraint or test on a branch.

  2. Look here first, in this order:

    • vendor/lmc/cqrs-bundle/README.md — the canonical usage example.
    • vendor/lmc/cqrs-bundle/DependencyInjection/Configuration.php — the real config keys and defaults (don't trust blog posts; this is a low-traffic package and docs lag code).
    • vendor/lmc/cqrs-bundle/DependencyInjection/*CompilerPass.php — shows which service tags are collected and how the handler map is built.
    • bin/console debug:container 'Lmc\Cqrs' — confirms the service IDs and interface aliases available for autowiring.
  3. First use case: a single "write" flow. Pick one controller action that currently does too much and convert it to a command:

    • Create RegisterUser implementing the package's command marker interface, with readonly constructor-promoted properties.
    • Create RegisterUserHandler implementing the command-handler interface, with a supports() method returning $command instanceof RegisterUser and a handle() method containing the logic.
    • Inject the command bus into the controller and call $commandBus->dispatch(new RegisterUser(...)).
    • Run bin/console debug:container --tag=<handler tag from the compiler pass> to confirm the handler was auto-registered.

    Get this one vertical slice working end-to-end before introducing queries or decorators.

Implementation Patterns

Commands and queries as immutable DTOs. Keep them dumb: constructor-promoted public readonly properties, no behaviour beyond simple getters/assertions. One class per use case (RegisterUser, ChangeEmail, GetUserProfile) rather than one per entity — this is what makes the handler map tractable and greppable.

One handler per command; supports() is the routing table. The typical handler shape is:

final class RegisterUserHandler implements CommandHandlerInterface
{
    public function __construct(private UserRepository $users) {}

    public function supports(CommandInterface $command): bool
    {
        return $command instanceof RegisterUser;
    }

    public function handle(CommandInterface $command): void
    {
        // $command is typed loosely; narrow it here or add a private assert
        $this->users->save(User::register($command->email));
    }
}

Because handle() receives the marker interface, add a \assert($command instanceof RegisterUser) (or a private handleRegisterUser()) so static analysis and your IDE stay useful.

Inject the bus, not the handler. Autowire the bus interface (CommandBusInterface / QueryBusInterface) into controllers, console commands, and listeners. This is the seam that lets you swap in a decorated bus later. If autowiring aliases aren't present in your Symfony version, bind them explicitly in config/services.yaml:

services:
    Lmc\Cqrs\Command\CommandBusInterface: '@lmc_cqrs.command_bus'
    Lmc\Cqrs\Query\QueryBusInterface: '@lmc_cqrs.query_bus'

(Verify exact IDs with debug:container; the pattern matters more than the literal ID.)

Queries return data; commands don't. Use the query bus from controllers/API endpoints and let handlers return arrays or read models directly. Resist returning Doctrine entities from query handlers — return DTOs so the read side can evolve independently of the write side.

Decorate the bus for cross-cutting concerns. The most valuable day-to-day pattern with this package is a decorator around the bus interface:

final class TransactionalCommandBus implements CommandBusInterface
{
    public function __construct(
        private CommandBusInterface $inner,
        private EntityManagerInterface $em,
    ) {}

    public function dispatch(CommandInterface $command): void
    {
        $this->em->wrapInTransaction(fn () => $this->inner->dispatch($command));
    }
}

Same trick for logging (command class + duration), metrics, and a validation layer. Register the decorator with decorates: lmc_cqrs.command_bus so nothing else changes.

Testing. Unit-test handlers directly — they're plain services — which is the main payoff of the pattern. Add one KernelTestCase integration test that dispatches each command through the real bus, so a missing or duplicate supports() is caught in CI rather than production.

CLI and scheduled jobs can reuse the same commands; this is a good second use case because it demonstrates the bus is transport-agnostic.

Gotchas and Tips

  • It is not Symfony Messenger. The bus is synchronous, in-process, with no transport, retry, or async support. If you need background processing, dispatch a Messenger message from inside the handler rather than expecting the CQRS bus to do it.
  • supports() is evaluated on every registered handler until one matches. Keep it to a cheap instanceof; never do I/O or container lookups there. Two handlers supporting the same command is a silent bug — the outcome depends on registration order, so add the integration test above.
  • Unregistered handler = runtime exception, not a compile error (unless the bundle ships a compiler pass that validates the map — check). Wire up a smoke test that dispatches every command once.
  • No validation or transaction middleware out of the box. Handlers receive raw commands. Add a validating decorator (Symfony Validator) and a transactional decorator; don't scatter wrapInTransaction across handlers.
  • Autoconfiguration depends on interface implementation. Handlers must implement the package's interfaces (or be explicitly tagged with the tag from the compiler pass) to be collected. If a handler "isn't found", check debug:container --tag=<tag> first, then the services.yaml _defaults: autoconfigure: true block.
  • Config keys are undocumented in places. Read Configuration.php before writing YAML; guessing lmc_cqrs: keys from the README is a common time sink on older tagged versions.
  • Maintenance risk is real. One star, zero dependents, and no release since 2022. Pin the version, vendor the bundle's config into your own repo if you patch it, and be prepared to fork or migrate to plain Messenger handlers if you hit a Symfony compatibility wall. For new greenfield projects, evaluate whether Symfony Messenger's handler middleware already covers your needs; adopt this package mainly if you want the explicit command/query split and a small dependency surface.
  • Extension point to remember: the handler map compiler pass. If you need dynamic handler registration (e.g. feature-flagged handlers), a small compiler pass adding tags is cleaner than editing services by hand.
  • Debugging checklist: debug:container --tag=... (is it registered?), a logging bus decorator (did it reach the bus?), and a dd() in supports() (did routing pick the right handler?). In that order.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource