esoul-cz / laravel-cqrs
Laravel simple CQRS implementation
Requires
- php: >=8.5
- esoul-cz/cqrs: ^1.0
- illuminate/support: ^10.2|^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.16.1
Requires (Dev)
- laravel/framework: ^12.48
- laravel/pail: ^1.2.2
- laravel/pint: ^1.13
- phpstan/phpstan: ^2
- phpunit/phpunit: ^13.1
This package is auto-updated.
Last update: 2026-04-28 11:40:04 UTC
README
Laravel integration for esoul-cz/cqrs. The package wires the command bus and query bus into the Laravel container, supports handler auto-discovery, and adds artisan generators for commands, queries, and their handlers.
Requirements
- PHP
>= 8.5 - Laravel
10.x,11.x,12.x, or13.x
Installation
Install directly from Packagist:
composer require esoul-cz/laravel-cqrs
Or install directly from GitHub:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/eSoul-cz/laravel-cqrs"
}
]
}
Publish the configuration:
php artisan laravel-cqrs:install
That command publishes config/cqrs.php.
What the package registers
The service provider registers these singletons in the Laravel container:
Esoul\Cqrs\Contracts\CommandBusInterfaceEsoul\Cqrs\Contracts\QueryBusInterface
Both buses use Laravel's container to instantiate handlers, so constructor injection works out of the box.
The package also ships facade classes:
Esoul\LaravelCqrs\Facades\CommandBusEsoul\LaravelCqrs\Facades\QueryBus
Default structure
The published config defaults to this application structure:
- commands:
app/Domain/CQRS/Command - queries:
app/Domain/CQRS/Query - command namespace:
App\Domain\CQRS\Command - query namespace:
App\Domain\CQRS\Query
The generators and handler discovery read these values from config/cqrs.php, so you can move the classes by changing the configured namespaces and paths.
Configuration
Published config lives in config/cqrs.php.
Discovery
By default, handler discovery is enabled:
'discovery' => [ 'enabled' => true, 'cache_dir' => storage_path('framework/cache/discovery'), 'paths' => [ 'base' => [ 'base_namespace' => 'App\\Domain\\CQRS', 'path' => app_path('Domain/CQRS'), ], ], ],
Each configured discovery path must define:
base_namespacepath
On boot, the package scans these paths for handlers marked with:
#[HandlesCommand(...)]#[HandlesQuery(...)]
Discovered handlers are registered into the Laravel container and mapped into the corresponding bus.
Manual registration
If you do not want discovery for some handlers, or you need explicit registration, use:
'commands' => [ 'register' => [ App\Domain\CQRS\Command\CreateOrderCommand::class => App\Domain\CQRS\Command\CreateOrderCommandHandler::class, ], ], 'queries' => [ 'register' => [ App\Domain\CQRS\Query\FindOrderQuery::class => App\Domain\CQRS\Query\FindOrderQueryHandler::class, ], ],
The package validates these mappings during boot:
- command classes must implement
CommandInterface - command handlers must implement
CommandHandlerInterface - query classes must implement
QueryInterface - query handlers must implement
QueryHandlerInterface
Invalid mappings fail fast with an InvalidArgumentException.
Usage
Dispatch a command
<?php declare(strict_types=1); namespace App\Domain\CQRS\Command; use Esoul\Cqrs\Contracts\CommandInterface; /** * @implements CommandInterface<string> */ final readonly class CreateOrderCommand implements CommandInterface { public function __construct( public string $number, ) {} }
<?php declare(strict_types=1); namespace App\Domain\CQRS\Command; use Esoul\Cqrs\Attributes\HandlesCommand; use Esoul\Cqrs\Contracts\CommandHandlerInterface; use Esoul\Cqrs\Contracts\CommandInterface; #[HandlesCommand(CreateOrderCommand::class)] final class CreateOrderCommandHandler implements CommandHandlerInterface { /** * @param CreateOrderCommand $command */ public function handle(CommandInterface $command): string { return 'Created order ' . $command->number; } }
<?php use App\Domain\CQRS\Command\CreateOrderCommand; use Esoul\Cqrs\Contracts\CommandBusInterface; $result = app(CommandBusInterface::class)->dispatch( new CreateOrderCommand('ORD-001') );
Using the facade:
<?php use App\Domain\CQRS\Command\CreateOrderCommand; use Esoul\LaravelCqrs\Facades\CommandBus; $result = CommandBus::dispatch( new CreateOrderCommand('ORD-001') );
Execute a query
<?php declare(strict_types=1); namespace App\Domain\CQRS\Query; use Esoul\Cqrs\Contracts\QueryInterface; /** * @implements QueryInterface<array{id: int, number: string}|null> */ final readonly class FindOrderQuery implements QueryInterface { public function __construct( public string $number, ) {} }
<?php declare(strict_types=1); namespace App\Domain\CQRS\Query; use Esoul\Cqrs\Attributes\HandlesQuery; use Esoul\Cqrs\Contracts\QueryHandlerInterface; use Esoul\Cqrs\Contracts\QueryInterface; #[HandlesQuery(FindOrderQuery::class)] final class FindOrderQueryHandler implements QueryHandlerInterface { /** * @param FindOrderQuery $query * @return array{id: int, number: string}|null */ public function handle(QueryInterface $query): ?array { return ['id' => 1, 'number' => $query->number]; } }
<?php use App\Domain\CQRS\Query\FindOrderQuery; use Esoul\Cqrs\Contracts\QueryBusInterface; $result = app(QueryBusInterface::class)->execute( new FindOrderQuery('ORD-001') );
Using the facade:
<?php use App\Domain\CQRS\Query\FindOrderQuery; use Esoul\LaravelCqrs\Facades\QueryBus; $result = QueryBus::execute( new FindOrderQuery('ORD-001') );
Artisan generators
The package provides four generators:
php artisan make:cqrs-command CreateOrder php artisan make:cqrs-command CreateOrder --handler php artisan make:cqrs-command-handler CreateOrderCommand php artisan make:cqrs-query FindOrder php artisan make:cqrs-query FindOrder --handler php artisan make:cqrs-query-handler FindOrderQuery
Generated file locations
- command classes use
cqrs.commands.path - command handler classes use
cqrs.command_handlers.path - query classes use
cqrs.queries.path - query handler classes use
cqrs.query_handlers.path
Their namespaces come from the matching base_namespace settings.
Naming conventions
The generators normalize names for you:
- commands get the
Commandsuffix - command handlers get the
Handlersuffix - queries get the
Querysuffix - query handlers get the
Handlersuffix
Examples:
make:cqrs-command CreateOrder=>CreateOrderCommandmake:cqrs-command-handler CreateOrder=>CreateOrderCommandHandlermake:cqrs-query FindOrder=>FindOrderQuerymake:cqrs-query-handler FindOrder=>FindOrderQueryHandler
Return types
make:cqrs-command and make:cqrs-query support --return and pass that type through to the generated phpdoc. Their handler generators also support --return to set the generated handle() return type.
Example:
php artisan make:cqrs-command CreateOrder --handler --return=string
php artisan make:cqrs-query FindOrder --handler --return='array{id:int,number:string}|null'
Notes
- Discovery and manual registration can be used together.
- Facades are class-based only; no alias is registered automatically.
- If no handler is registered for a dispatched command or executed query, the underlying bus throws a runtime exception.
- Handler discovery depends on the
esoul-cz/cqrsattributes, so handlers must be annotated correctly.