Implementation of Event-Sourcing / CQRS approach in api-platform - cqrs

on official Api-Platform website there is a General Design Considerations page.
Last but not least, to create Event Sourcing-based systems, a convenient approach is:
to persist data in an event store using a custom data persister
to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
to map those projections with read-only Doctrine entity classes and to mark those classes with #ApiResource
You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins, etc provided by API Platform.
So, I tried to implement this approach with one simplification (one DB is used, but with separated reads and writes).
But failed... there is a problem, which I don't know how to resolve, so kindly asking you for a help!
I created a User Doctrine entity and annotated fields I want to expose with #Serializer\Groups({"Read"}). I will omit it here as it's very generic.
User resource in yaml format for api-platform:
# config/api_platform/entities/user.yaml
App\Entity\User\User:
attributes:
normalization_context:
groups: ["Read"]
itemOperations:
get: ~
collectionOperations:
get:
access_control: "is_granted('ROLE_ADMIN')"
So, as it's shown above User Doctrine entity is read-only, as only GET methods are defined.
Then I created a CreateUser DTO:
# src/Dto/User/CreateUser.php
namespace App\Dto\User;
use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
final class CreateUser
{
/**
* #var string
* #Assert\NotBlank()
* #Assert\Email()
* #AppAssert\FakeEmailChecker()
*/
public $email;
/**
* #var string
* #Assert\NotBlank()
* #AppAssert\PlainPassword()
*/
public $plainPassword;
}
CreateUser resource in yaml format for api-platform:
# config/api_platform/dtos/create_user.yaml
App\Dto\User\CreateUser:
itemOperations: {}
collectionOperations:
post:
access_control: "is_anonymous()"
path: "/users"
swagger_context:
tags: ["User"]
summary: "Create new User resource"
So, here you can see that only one POST method is defined, exactly for creation of a new User.
And here what router shows:
$ bin/console debug:router
---------------------------------- -------- -------- ------ -----------------------
Name Method Scheme Host Path
---------------------------------- -------- -------- ------ -----------------------
api_create_users_post_collection POST ANY ANY /users
api_users_get_collection GET ANY ANY /users.{_format}
api_users_get_item GET ANY ANY /users/{id}.{_format}
I also added a custom DataPersister to handle POST to /users. In CreateUserDataPersister::persist I used Doctrine entity to write data, but for this case it doesn't matter as Api-platform do not know anything about how DataPersister will write it.
So, from the concept - it's a separation of reads and writes.
Reads are performed by Doctrine's DataProvider shipped with Api-platform, and writes are performed by custom DataPersister.
# src/DataPersister/CreateUserDataPersister.php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\User\CreateUser;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;
class CreateUserDataPersister implements DataPersisterInterface
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function supports($data): bool
{
return $data instanceof CreateUser;
}
public function persist($data)
{
$user = new User();
$user
->setEmail($data->email)
->setPlainPassword($data->plainPassword);
$this->manager->persist($user);
$this->flush();
return $user;
}
public function remove($data)
{
}
}
When I perform a request to create new User:
POST https://{{host}}/users
Content-Type: application/json
{
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
Problem!
I'm getting a 400 response ... "hydra:description": "No item route associated with the type "App\Dto\User\CreateUser"." ...
However, a new record is added to database, so custom DataPersister works ;)
According to General Design Considerations separations of writes and reads are implemented, but not working as expected.
I'm pretty sure, that I could be missing something to configure or implement. So, that's why it's not working.
Would be happy to get any help!
Update 1:
The problem is in \ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolver::getRouteName(). At lines 48-59 it iterates through all routes trying to find appropriate route for:
$operationType = 'item'
$resourceClass = 'App\Dto\User\CreateUser'
But $operationType = 'item' is defined only for $resourceClass = 'App\Entity\User\User', so it fails to find the route and throws an exception.
Update 2:
So, the question could sound like this:
How it's possible to implement separation of reads and writes (CQS?) using Doctrine entity for reads and DTO for writes, both residing on the same route, but with different methods?
Update 3:
Data Persisters
store data to other persistence layers (ElasticSearch, MongoDB, external web services...)
not publicly expose the internal model mapped with the database through the API
use a separate model for read operations and for updates by implementing patterns such as CQRS
Yes! I want that... but how to achieve it in my example?

Short Answer
The problem is that the Dto\User\CreateUser object is getting serialized for the response, when in fact, you actually want the Entity\User to be returned and serialized.
Long Answer
When API Platform serializes a resource, they will generate an IRI for the resource. The IRI generation is where the code is puking. The default IRI generator uses the Symfony Router to actually build the route based on the API routes created by API Platform.
So for generating an IRI on an entity, it will need to have a GET item operation defined because that is the route that will be the IRI for the resource.
In your case, the DTO doesn't have a GET item operation (and shouldn't have one), but when API Platform tries to serialize your DTO, it throws that error.
Steps to Fix
From your code sample, it looks like the User is being returned, however, it's clear from the error that the User entity is not the one being serialized.
One thing to do would be to install the debug-pack, start the dump server with bin/console server:dump, and add a few dump statements in the API Platform WriteListener: ApiPlatform\Core\EventListener\WriteListener near line 53:
dump(["Controller Result: ", $controllerResult]);
$persistResult = $this->dataPersister->persist($controllerResult);
dump(["Persist Result: ", $persistResult]);
The Controller Result should be an instance of your DTO, the Persist Result should be an instance of your User entity, but I'm guessing it's returning your DTO.
If it is returning your DTO, you need to just debug and figure out why the DTO is being returned from the dataPersister->persist instead of the User entity. Maybe you have other data persisters or things in your system that can be causing conflict.
Hopefully this helps!

You need to send the "id" in your answer.
If User is Doctrine entity, use:
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
If User isn't Doctrine entity, use:
/**
* #Assert\Type(type="integer")
* #ApiProperty(identifier=true)
*/
private $id;
Anyway, your answer would be like this:
{
"id": 1, // Your unique id of User
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
P.S.: sorry for my english :)

Only Work in the 2.4 version but really helpful.
Just add
output_class=false for the CreateUserDTO and everything will be fine for POST|PUT|PATCH
output_class to false allow you to bypass the get item operation. You can see that in the ApiPlatform\Core\EventListener#L68.

Related

Are declarations of entire responses reusable in swagger-php?

I would love a way to declare some responses that are the same for every endpoint; for example every endpoint will output a 401 if you don't have a proper token, and then there can be another group of endpoints that will all output 404 if you don't have a valid id of something in the beginning of your path.
So for almost every method I find myself copy-pasting things such as this:
* #OA\Response(response=401, description="If no token..."),
* #OA\Response(response=404, description="If ID of thing invalid..."),
The getting started section says that reusable responses are supported, but covering only the properties (of successful responses).
There are no examples for the kind of thing i'm looking for, and I also can't get myself to guess anything that would compile.
Something like #OA\Response(ref="#/components/schemas/Unauthorized") seems like it should be the way (and the compilation doesn't complain about the presence of the ref attribute), but how do i then declare what the Unauthorized schema looks like? Because declaring a #OA\Response says it only expects the fields: "ref", "response", "description", "headers", "content", "links", "x", none of which can serve as an identifier.
I am using this in conjunction with L5-swagger for Laravel support.
Declare a response outside of an operation:
/**
* #OA\Response(response="Unauthorized", description="If no token...")
*/
This will add it to the #/components/responses/ using the response= as value as key.
Then you're able to reuse it later inside an operation:
/**
* #OA\Get(
* path="/example",
* #OA\Response(response=401, ref="#/components/responses/Unauthorized")
* )
*/

How can I add simple CMS functionality to existing Symfony application

I have an existing web application accessing a MySQL database. I'm porting this application to Symfony. The new application has to use the old database, as we cannot port the whole application at once, i.e. the old and the new application are accessing the same database and the applications are running simultaneously.
The old application had a simple CMS functionality which has to be ported:
There is a table pagewhich represents a page tree. Every page has a slug field. The URL path consists of those slugs representing the path identifying the page node, e.g. "/[parent-slug]/[child-slug]".
The page table also contains a content field. As I already mentioned, the CMS functionality is very simple, so the content is just rendered as page content inside a page layout. The page entry also specifies the page layout / template.
My problem is that I don't know how to set up the routing. In a normal Symfony application I'd know the URL patterns before, but in this case they are dynamic. Also routes cannot be cached, because they could be changed any time by the user. I wonder if I have to drop Symfony's routing completely and implement something on my own. But how?
Now I found Symfony CMF which tells a lot about the framework VS CMS routing conflict. So first, I thought this would be the right way. However the tutorials aim at building an entirely new application based on PHPRC. I wasn't able to derive the tutorial's concepts to my use case.
since you run several URL rules on one symfony application, you will need to work with url prefixes. Either your cms should work with a prefix /cms/parent-slug/child-slug or all other controllers. Otherwise you are not able to differ which controller is meant when a dynamic request arrives.
You can try a workaround with a KernelControllerListener. He will catch up every request and then check if a cms page is requested. On the basis of the request you can set controller and action by yourself. Concept:
Create only one route with "/". Abandon oll other rules. Then create a Listener like this:
<?php
namespace AppBundle\Listener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* Class KernelControllerListener
* #package ApiBundle\Listener
*/
class KernelControllerListener
{
/**
* #var CmsRepository
*/
private $requestParser;
/**
* KernelControllerListener constructor.
* #param CmsRepository $CmsRepository
*/
public function __construct(CmsRepository $CmsRepository)
{
$this->CmsRepository = $CmsRepository;
}
/**
* #param FilterControllerEvent $event
*/
public function onKernelController(FilterControllerEvent $event){
$request = $event->getRequest();
//should be /parent-slug/children/slug or any other path
$path = $request->getPathInfo();
if($this->CmsRepository->getCmsControllerIfMatch($path)){
//cms repository search in db for page with this path, otherwise return false
$event->setController([AppBundle\CmsController::class, 'cmsAction']);
return;
}
//repeat if clause for any other application part
}
}
in services.yml:
app.controller_listener:
class: AppBundle\Listener\KernelControllerListener
arguments:
- "#app.cms_repository"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Edit: catch all routes, see https://www.jverdeyen.be/symfony2/symfony-catch-all-route/
The question is: Do you whant to migrate the data or not. For both question, the CMF can be an answer. If you wanna a simple dynamic router, you should have a look into the ChainRouter with an custom router definition:
https://symfony.com/doc/current/cmf/bundles/routing/dynamic.html
and
https://symfony.com/doc/current/cmf/components/routing/chain.html
If you wanna migrate the data, you can use fixture loaders, as we use in almost all of our examples.

In Symfony2 what is the best way to use a multi word name and get a good RESTful url

I'm using the FOSRestBundle to build my symfony2 API.
I have entities called things like SupportRequestTemplate, which I would like to see in the API end point but when I create the Actions with names like
getSupportRequestTemplateAction(Request $request, $id) {}
FOSRest treats each camel case word as a new Resource name, so the url I get is
/api/supports/{id}/request/template.json
which looks pretty bad. Is there a way to get the end points to look like this instead.
/api/support-request-templates/{id}.json
Or will I just have to suck it up and go to all lower case for something like
/api/supportrequesttemplates/{id}.json
You can use annotations to customise the url according to your needs.
For example #Get:
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* #Rest\Get("/api/support-request-templates/{id}.json")
*/
public function getSupportRequestTemplateAction($id, Request $request)
{
...
}
You can read more about these annotations in manual definition of routes.

New entity property names using FOS Rest and Nelmio Docs

I'm creating a REST API in Symfony2 using FOS REST and Nelmio docs.
/**
* #ApiDoc(
* description="Create a new Object",
* input="Your\Namespace\Form\Type\YourType",
* output="Your\Namespace\Class"
* )
*/
public function postAction()
{
}
Unfortunately this returns the output Class database field names, not the Class property names. We want our users to have consistent, descriptive and nice property names that we set in the entity Class, rather than our legacy field names in the database.
Similarly, when we GET the Class object, the wrong field names are in the returned object. Any thoughts on how to change this? Thanks!

Sails.js: Sending a 409 response if a duplicate record is posted

Not sure how to do this in sails.js, but I'd like to be able to, when creating a new object on the API, check to see if that object's id exists and if it does, send a 409 conflict response, and if it doesn't, create the object like normal.
For the sake of discussion, I've created a Brand model.
I'm assuming that I would override the create function in the BrandController, search for the brand based on req.param('id') and if it exists, send the error response. But I'm not sure if I'm doing this correctly, as I can't seem to get anything to work.
Anyone have ideas?
I ended up using a policy for this particular use case.
Under config/policies, I created a isRecordUnique policy:
/**
* recordIsUnique
*
* #module :: Policy
* #description :: Simple policy to check that a record is unique
*
* #docs :: http://sailsjs.org/#!documentation/policies
*
*/
module.exports = function(req, res, next) {
Brand.findOne({ id: req.body.id}).done(function (err, brand) {
if (brand) {
res.send(409);
} else {
next();
}
});
};
This allowed me to avoid overriding any CRUD functions and it seemed to fit the definition of a policy, in that only checks one thing.
To tie my policy to my create function, I modified config/policies by adding:
BrandController: {
create: 'isRecordUnique'
}
That's it. Took me way too long to figure this out, but I think it's a good approach.
Well since this is MVC you are thinking correctly that the Control should be enforcing this logic. However, as this is basic uniqueness by the primary id the model should know/understand and help enforce this.
Model should identity the conflict.
In sails the coder is responsible for the defining uniqueness, but I would have the model object do it not the controller.
The controller should route/respond by sending the view which is effectively http 409.
Yes the controller create method should be used in this case, as sails wants to provide CRUD routes for you. Assuming it is a logical create not some resultant or odd non-restful side effect.
I think of Sails.js by default providing a model controller, so use their perspective since you are using their framework. There are many approaches to Control/Model relationships.
res.view([view, options[, fn]])
Ideally the view would control the http response code, the message, any special additional headers. The view just happens to be extremely basic, but could vary in the future.
You could always set headers and response with JSON from the controller but views offer you flexibility in the future, like decoupling, the reason the MVC pattern exists. However, sails also seems to value convenience, so if it is a small app maybe directly from the controller.