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.
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.
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.First use case: a single "write" flow. Pick one controller action that currently does too much and convert it to a command:
RegisterUser implementing the package's command marker interface, with readonly constructor-promoted properties.RegisterUserHandler implementing the command-handler interface, with a supports() method returning $command instanceof RegisterUser and a handle() method containing the logic.$commandBus->dispatch(new RegisterUser(...)).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.
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.
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.wrapInTransaction across handlers.debug:container --tag=<tag> first, then the services.yaml _defaults: autoconfigure: true block.Configuration.php before writing YAML; guessing lmc_cqrs: keys from the README is a common time sink on older tagged versions.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.How can I help you explore Laravel packages today?