CQRS with API Platform
# CQRS with Symfony
Here, CQRS stands for Command and Query Responsibility Segregation. I recommend to read Martin Fowler’s introduction which is a really good starting point.
I often see implementation of CQRS using the Symfony messenger component. Let’s take two data transfer objects an OrderBookCommand
and a ReadBookQuery
. For each of these objects we have message handlers, in our case an OrderBookCommandHandler
and a ReadBookQueryHandler
.
Then, using the messenger we dispatch our query/command class through a bus, which calls handlers. Let’s look at the code to order a book:
use App\Query\ReadBookQuery;
use App\MessageHandler\Query\ReadBookQueryHandler;
use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
$query = new ReadBookQuery();
$handler = new ReadBookQueryHandler();
$bus = new MessageBus([
new HandleMessageMiddleware(new HandlersLocator([
ReadBookQuery::class => [$handler],
])),
]);
$bus->dispatch($query);
For commands the code is alike. This works well, gives you re-usability and maintains the responsibility segregation.
# CQRS with Api Platform
API Platform is, at his heart, built based on the CQRS pattern. Indeed, the Command and Query Responsibility Segregation hides behind our concepts of State Provider and State Processor.
We often notice the previous messenger implementation on top of API Platform. The implementation usually uses a custom Provider, that calls the messenger bus to do all the above. We therefore see twice as much classes for a query:
- a Resource (
Book
) - the query Data Transfer Object (
ReadBookQuery
) - the query Handler (
ReadBookQueryHandler
) - the query Provider (
ReadBookQueryProvider
)
A more concise way of doing so would be to use a DTO
, that represents my Resource on which I use my Handler, for querying it would look like this:
#[Get('/books/{id}', provider: ReadBookQueryHandler::class)]
class Book {
public $id;
}
The same goes for a Command, let’s use a RPC-like POST operation:
#[Post('/books/{id}/order', processor: OrderBookCommandHandler::class)]
class Book {
public $id;
}
In my opinion, using messenger with API Platform brings too much complexity for no benefits. Note that there are cases where using messenger is still relevant, for example when you want to do asynchronous tasks or if you want to go full Domain-Driven Design where it will help layering the code (check how we do). Still this is probably overkill for most projects.
At last, when using API Platform you can definitely re-use the CQRS pattern. Let’s build a script that orders a book and check the library:
$provider = new LibraryProvider();
$processor = new OrderBookProcessor();
$book = new Book();
$book = $processor->process($book, new Post('/books/{id}/order'));
$library = $provider->provide(new Get('/books'));
Or even by using HTTP:
use Symfony\Component\HttpFoundation\Request;
$orderBook = Request::create('/books/1/order', content: '{"price": "10"}', method: 'POST');
$response = $kernel->handle($orderBook);
$orderBook = Request::create('/books');
$response = $kernel->handle($orderBook);
Keep it simply stupid. Let me know your thoughts on this in the comments.