Installation
composer require boltconcepts/bdev-cqrs-bundle
Add to config/bundles.php:
BoltConcepts\BDevCQRSBundle\BDevCQRSBundle::class => ['all' => true],
First Use Case: Command Handling
Define a command class (e.g., src/Command/CreateUserCommand.php):
namespace App\Command;
use BoltConcepts\BDevCQRSBundle\Command\CommandInterface;
class CreateUserCommand implements CommandInterface {
public $email;
public $name;
}
Create a handler (e.g., src/CommandHandler/CreateUserCommandHandler.php):
namespace App\CommandHandler;
use App\Command\CreateUserCommand;
use BoltConcepts\BDevCQRSBundle\Command\CommandHandlerInterface;
class CreateUserCommandHandler implements CommandHandlerInterface {
public function handle(CreateUserCommand $command) {
// Business logic here
return true;
}
}
Dispatching Commands
Inject CommandBus into a controller/service:
use BoltConcepts\BDevCQRSBundle\Command\CommandBus;
class UserController {
public function __construct(private CommandBus $commandBus) {}
public function createUser() {
$command = new CreateUserCommand();
$command->email = 'user@example.com';
$command->name = 'John Doe';
$this->commandBus->dispatch($command);
}
}
Query Handling (Optional)
Define a query (e.g., src/Query/GetUserQuery.php):
namespace App\Query;
use BoltConcepts\BDevCQRSBundle\Query\QueryInterface;
class GetUserQuery implements QueryInterface {
public $userId;
}
Create a query handler (e.g., src/QueryHandler/GetUserQueryHandler.php):
namespace App\QueryHandler;
use App\Query\GetUserQuery;
use BoltConcepts\BDevCQRSBundle\Query\QueryHandlerInterface;
class GetUserQueryHandler implements QueryHandlerInterface {
public function handle(GetUserQuery $query) {
// Fetch user data
return ['id' => $query->userId, 'name' => 'John Doe'];
}
}
Dispatching Queries
Inject QueryBus into a controller/service:
use BoltConcepts\BDevCQRSBundle\Query\QueryBus;
class UserController {
public function __construct(private QueryBus $queryBus) {}
public function getUser($id) {
$query = new GetUserQuery();
$query->userId = $id;
return $this->queryBus->ask($query);
}
}
CreateUserCommand, UpdateUserCommand).GetUserByEmailQuery, ListActiveUsersQuery).CommandBus and queries via QueryBus. Avoid mixing logic in controllers.The bundle supports events for side effects (e.g., logging, notifications). Extend BoltConcepts\BDevCQRSBundle\Event\EventInterface and configure listeners in config.yml:
bolt_concepts_bdev_cqrs:
event_listeners:
App\EventListener\UserCreatedListener: ['onUserCreated']
Add middleware to pre/post-process commands or queries. Implement BoltConcepts\BDevCQRSBundle\Command\CommandMiddlewareInterface or BoltConcepts\BDevCQRSBundle\Query\QueryMiddlewareInterface:
class LoggingMiddleware implements CommandMiddlewareInterface {
public function handle(CommandInterface $command, callable $next) {
\Log::info('Command dispatched: ' . get_class($command));
return $next($command);
}
}
Register in services.yml:
services:
App\Middleware\LoggingMiddleware:
tags:
- { name: bolt_concepts_bdev_cqrs.command_middleware }
Wrap command handling in transactions for data consistency:
use Doctrine\DBAL\Connection;
class CreateUserCommandHandler implements CommandHandlerInterface {
public function __construct(private Connection $connection) {}
public function handle(CreateUserCommand $command) {
$this->connection->beginTransaction();
try {
// Business logic
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
}
}
Leverage Symfony’s DI to inject services into command/query handlers:
class CreateUserCommandHandler implements CommandHandlerInterface {
public function __construct(
private UserRepository $userRepository,
private MailerInterface $mailer
) {}
public function handle(CreateUserCommand $command) {
$user = $this->userRepository->save($command);
$this->mailer->sendWelcomeEmail($user);
}
}
Use Symfony’s Validator for command/query validation:
use Symfony\Component\Validator\Constraints as Assert;
class CreateUserCommand implements CommandInterface {
/**
* @Assert\NotBlank
* @Assert\Email
*/
public $email;
/**
* @Assert\NotBlank
* @Assert\Length(min: 2)
*/
public $name;
}
Configure validation in config.yml:
bolt_concepts_bdev_cqrs:
command_validators: true
query_validators: false
Bundle Enablement
Ensure the bundle is enabled in config/bundles.php. Without this, services (CommandBus, QueryBus) won’t be autowired.
Service Naming
The bundle uses bolt_concepts_bdev_cqrs as the root config key. Typos here will cause silent failures.
Command/Query Naming
Avoid naming conflicts with Symfony’s built-in services. Prefix custom commands/queries (e.g., App\Command\CreateUserCommand).
Unhandled Commands/Queries
If a command/query has no handler, the bundle throws a CommandNotFoundException or QueryNotFoundException. Verify:
CommandHandlerInterface or QueryHandlerInterface.Circular Dependencies Avoid circular dependencies between command/query handlers. Use interfaces and abstract classes to decouple logic.
Transaction Deadlocks Long-running commands may cause deadlocks. Keep transactions short and avoid nested transactions.
foreach ($users as $user) {
$this->commandBus->dispatch(new CreateUserCommand($user));
}
Consider using a queue system (e.g., Symfony Messenger) for high-throughput scenarios.
2. **Query Caching**
Cache frequent queries (e.g., `GetUserQuery`) using Symfony’s cache component:
```php
use Symfony\Contracts\Cache\CacheInterface;
class GetUserQueryHandler implements QueryHandlerInterface {
public function __construct(private CacheInterface $cache) {}
public function handle(GetUserQuery $query) {
return $this->cache->get($query->userId, function() use ($query) {
return $this->userRepository->find($query->userId);
});
}
}
# config/services.yaml
App\CommandHandler\RareCommandHandler:
autowire: true
lazy: true
Custom Command/Query Buses
Extend the default CommandBus or QueryBus to add custom logic:
class CustomCommandBus extends CommandBus {
public function dispatch(CommandInterface $command) {
// Pre-processing
parent::dispatch($command);
// Post-processing
}
}
Register as a service and override the default bus.
Dynamic Handlers Use Symfony’s compiler passes to dynamically register handlers based on runtime conditions (advanced use case).
Event Dispatching Extend the bundle’s event system to trigger custom events after command/query execution:
class CreateUserCommandHandler implements CommandHandlerInterface {
public function __construct(private EventDispatcherInterface $dispatcher) {}
public function handle(CreateUserCommand $command) {
$user = $this->userRepository->save($command);
$this->dispatcher->dispatch(new UserCreatedEvent($user));
How can I help you explore Laravel packages today?