Installation
composer require digital-craftsman/cqs-routing
Add the config file to config/packages/cqs-routing.php (see README for template).
First Use Case: Command/Query Handling
Define a command/query class (e.g., CreateUserCommand):
namespace App\Application\Commands;
class CreateUserCommand
{
public function __construct(
public string $name,
public string $email
) {}
}
Register a handler (implement CommandHandlerInterface):
namespace App\Application\Handlers;
use App\Application\Commands\CreateUserCommand;
use DigitalCraftsman\CQSRouting\CommandHandlerInterface;
class CreateUserCommandHandler implements CommandHandlerInterface
{
public function __invoke(CreateUserCommand $command): void
{
// Business logic here
}
}
Route Configuration
Add a route in routes/api.php:
use DigitalCraftsman\CQSRouting\CQSRouting;
use App\Application\Commands\CreateUserCommand;
Route::post('/users', CreateUserCommand::class)
->middleware(CQSRouting::class);
Request Payload Send a JSON payload matching the command structure:
{
"name": "John Doe",
"email": "john@example.com"
}
Command/Query Separation
CreateUserCommand, UpdateOrderCommand).GetUserQuery, ListOrdersQuery).// Command (write)
Route::post('/orders/{id}/cancel', CancelOrderCommand::class);
// Query (read)
Route::get('/orders/{id}', GetOrderQuery::class);
DTO Validation
Leverage the DTOValidator to enforce request structure:
// In config/cqs-routing.php
'dto_validator' => [
'class' => AccessValidator::class,
'parameters' => AccessValidatorParameters::class,
],
Customize validation rules in AccessValidatorParameters:
namespace App\Application\DTOValidator\DTO;
use Symfony\Component\Validator\Constraints as Assert;
class AccessValidatorParameters
{
#[Assert\NotBlank]
public string $name;
#[Assert\Email]
public string $email;
}
Response Handling
Configure responses via ResponseConstructor:
// For JSON APIs
'response_constructor' => SerializerJsonResponseConstructor::class,
// For empty responses (e.g., commands)
'response_constructor' => EmptyJsonResponseConstructor::class,
Transaction Management
Wrap handlers in ConnectionTransactionWrapper for database transactions:
// In config/cqs-routing.php
'handler_wrapper' => ConnectionTransactionWrapper::class,
Middleware Integration Chain CQS middleware with Laravel’s middleware stack:
Route::middleware([CQSRouting::class, 'auth:sanctum'])->post('/users', CreateUserCommand::class);
Serializer, Validator). Use Laravel’s service container to bind Symfony services.$handler = $this->createMock(CommandHandlerInterface::class);
$handler->method('__invoke')->willReturn(new ResponseDto(...));
$this->app->instance(
CreateUserCommandHandler::class,
$handler
);
ExceptionHandler to customize error responses:
namespace App\Application\ExceptionHandlers;
use DigitalCraftsman\CQSRouting\ExceptionHandlerInterface;
use Symfony\Component\HttpFoundation\Response;
class CustomExceptionHandler implements ExceptionHandlerInterface
{
public function __invoke(\Throwable $exception): Response
{
return new Response(
json_encode(['error' => $exception->getMessage()]),
400
);
}
}
Register in config:
'exception_handler' => CustomExceptionHandler::class,
Circular Dependencies Avoid circular references between commands/queries and their handlers. Use interfaces for handlers to decouple implementations:
interface CancelOrderHandlerInterface
{
public function __invoke(CancelOrderCommand $command): void;
}
Request Decoding
Ensure payloads match the DTO structure exactly. Use JsonRequestDecoder for JSON APIs, but validate manually for custom formats:
// Custom decoder example
'request_decoder' => CustomRequestDecoder::class,
Transaction Scope
ConnectionTransactionWrapper rolls back on exceptions. Handle exceptions explicitly in handlers to avoid silent failures:
try {
$this->entityManager->persist($user);
$this->entityManager->flush();
} catch (\Exception $e) {
throw new \RuntimeException('Failed to create user', 0, $e);
}
Performance Avoid heavy validation or serialization in handlers. Offload complex logic to services:
// Handler
public function __invoke(CreateUserCommand $command)
{
$this->userService->create($command->name, $command->email);
}
Route Caching Clear route cache after adding new CQS routes:
php artisan route:clear
Handler Not Found
Verify the handler is registered as a service and implements CommandHandlerInterface/QueryHandlerInterface. Check for typos in class names.
Validation Errors Enable Symfony’s validator debug mode:
// In config/services.php
'validator' => [
'debug' => env('APP_DEBUG', false),
],
Check logs for constraint violations.
Serialization Issues
Ensure DTOs are serializable. Use #[SerializedName] for custom field names:
use JMS\Serializer\Annotation as Serializer;
class CreateUserCommand
{
#[Serializer\SerializedName('user_name')]
public string $name;
}
Middleware Order
Place CQSRouting middleware after auth/validation middleware to avoid redundant checks:
Route::middleware(['throttle:60', CQSRouting::class])->post('/users', CreateUserCommand::class);
Custom Request Decoders
Implement RequestDecoderInterface for non-JSON payloads (e.g., form data):
namespace App\Application\RequestDecoders;
use DigitalCraftsman\CQSRouting\RequestDecoder\RequestDecoderInterface;
use Symfony\Component\HttpFoundation\Request;
class FormRequestDecoder implements RequestDecoderInterface
{
public function decode(Request $request, string $commandClass): mixed
{
return new $commandClass(
$request->request->get('name'),
$request->request->get('email')
);
}
}
Register in config:
'request_decoder' => FormRequestDecoder::class,
Dynamic Handlers Use Laravel’s service providers to bind handlers dynamically:
// In AppServiceProvider
public function register()
{
$this->app->bind(
CreateUserCommandHandler::class,
fn () => new CreateUserCommandHandler($this->app->make(UserRepository::class))
);
}
Event Dispatching
Extend handlers to dispatch events (e.g., UserCreatedEvent):
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class CreateUserCommandHandler implements CommandHandlerInterface
{
public function __construct(
private EventDispatcherInterface $dispatcher
) {}
public function __invoke(CreateUserCommand $command): void
{
$user = $this->userRepository->create($command);
$this->dispatcher->dispatch(new UserCreatedEvent($user));
}
}
Logging Add logging to handlers for observability:
use Psr\Log\LoggerInterface;
class CreateUserCommandHandler
{
public function __construct(private LoggerInterface $logger) {}
public function __invoke(CreateUserCommand $command): void
{
$this->logger->info('Creating user', ['email' => $command->email]);
// ...
}
}
How can I help you explore Laravel packages today?