Domain-driven-design: Moving forward with API Platform and Elasticsearch
TLDR; it’s awesome!
In my previous post I showed you how to create a
Car domain layer really fast. But there was also this footnote:
Well … it’s here 🎉, check out this Symfony demo application for the current proof of concept.
Up till now our car domain contains entities and repositories, nicely decoupled from e.g. Doctrine ORM.
In this post I’ll talk about the following concepts for the API part of it:
- API Platform
- API resources & projections
- Projection documents & Elasticsearch
It’s great! Love it. Perhaps a bit too much focused on auto-mapping with Doctrine ORM (potential footgun also 🙊), but we can nicely workaround that.
Let’s install it:
composer create-project symfony/skeleton app && cd app
composer require api
To have a quick look we’ll run a webserver:
php -S 127.0.0.1:8000 -t public
Now broswing to
http://localhost:8000/api … and voila!
API Resources & Projections
So, the bigger picture is to create a Car API based on projections, reflected from our Car entity.
What’s a “projection”?
Put simple; it’s just a PHP object, a model. It’s a well known strategy used in Event-Sourcing landscape, where you are practically forced to create these projection models in an effort to get a usable presentation from your domain data (the source of truth), as that’s built from individual domain events.
Nevertheless, we can apply it anyway 😉, it’s really powerful. Here are some articles where you can read more about it:
So at this point we start decoupling from using entities as API resources, and instead we’ll use specialized projection models. Thus allows us to define optimized API responses.
It means we need to:
- transform entities into projections
- index projections in any document store
- provide projections to API Platform as a resource, using so called data providers
- have a synchronization process between entity changes and their projections
The overall data processing goes from entities to documents to projections. A document is our man-in-the-middle format, and is a nested array of scalar data.
The Car Projection
We need this one first, as API Platform will use it as its API resource. We continue to use the Base domain layer package we built our initial Car domain with.
We configure API Platform as follow:
If we refresh the API documentation we’ll see:
Great, so far so good 😃
For now only
GET operations are enabled as the whole approach is still a proof of concept and not all parts have been tackled yet. I created an issue to track the remaining steps, so feel free to help out.
The Car Document Transformer
A simple callable that transforms the entity into a projection document. By using an invokable object we can also treat it as a service. Or put different; we don’t have to define everything at the entity level, instead we can inject another service to resolve more document fields from.
An important thing is to create a deterministic document ID value. It should not change when transforming the same Car entity multiple times as it practically means we create new API permalinks each time, invalidating old ones.
We’ll use a single identifier for convenience. Also because I have no real experience using API Platform with composite identifiers, yet 😅
Projection documents & Elasticsearch
The good news is the basic application code is ready. Now we only have to setup a document store and start indexing our API.
Elasticsearch is a powerful search and analytics engine which comes with a tailored PHP API. For the Message driven PHP project I already created the minimal infrastructure we need. It currently requires to be wired manually, but yeah, only configuration from here on:
And done! The main services we wired are:
- 2 standard CLI commands to initialize and synrchonize
And have a look at your API 😉, assuming you have some Car entities saved before.
Each time a entity changes you’ll need to run
domain:projection:synchronize. The next step is to automatically update / invalidate projections based on entity changes of course.
You can read more about the whole approach in the main documentation.
I’m pretty happy with the results so far 😏
Cheers! And thanks for reading.
edit: Continue reading the next part