Pragmatic architecture 3: Argument value resolvers

Dev Diary

Second part of the plan: Get rid of a lot of manual work! In the previous part I explained our file structure, routing with the REST endpoints and how our controllers work. In this part we are going to have a look at how to pass those automatically created command and query objects to the controller actions.

Autor
Michael Zangerle
Datum
3. März 2021
Lesedauer
4 Minuten

In Symfony there exists a concept called “argument value resolvers“ which is used to inject parameters like the request into a controller action. In our case we are less interested in the request object itself, but more on how to get the data from the request object into our command and query objects e.g. GetCustomerQuery $query and ActivateCustomerCommand $command in our controller.

Let’s have a look how they work and add our custom resolver.

How do they work?

As described in the documentation we need to create a class that implements the Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface and register this class as a service with a controller.argument_value_resolver.

The interface forces us to implement the two methods supports and resolve. When the argument resolver is registered and a controller action with a parameter is called, Symfony will go through all argument resolvers and check which one supports the parameter in question. If a parameter (of this type) is supported the resolve method will be called and the expected value will be passed on to the called action as parameter.

Implementation and usage

At the time of this writing there exists no official argument value resolver for what we wanted to achieve so we implemented our own. It can be found here and you are welcome to make use of it.

Basically it will take the request payload and map it onto an object by using the Symfony Serializer. Routing and query parameters will also be taken into account, depending on the request method. Afterwards the created object will be validated with the Symfony Validator. If it’s valid it will be injected into the controller action. If it’s not valid an exception will be thrown.

That’s basically what this class is doing and all we need to know to get started. We will get into the details in a separate blog post.

Query and command objects

Before we can try the whole thing out, we need to create some commands and queries. For our example we will get back to the GetCustomerQuery and ActivateCustomerCommand.

As we are already very specific with the name of our ActivateCustomerCommand we don’t need much information inside this class to know what to do with it in our business logic. To be more precise we only need the id of the customer. That’s why it looks like this:

1 <?php 2 3 namespace App\Customer\Message\Command; 4 5 // ... 6 7 final class ActivateCustomerCommand implements RequestDto 8 { 9 /** 10 * @Assert\NotNull(message="Id should not be null.") 11 * @Assert\Positive(message="Id should be a positive integer.") 12 */ 13 private int $id; 14 15 public function getId(): int 16 { 17 return $this->id; 18 } 19 20 public function setId(int $id): void 21 { 22 $this->id = $id; 23 } 24 }

Aside from that we just need to add the RequestDto marker interface which tells the argument resolver to handle this argument. This would be a perfect use case for PHP 8 attributes which we are going to add in the future and deprecate this marker interface.

Pretty much the same works for the GetCustomerQuery. As long as the route parameter name matches the property inside this class, the serializer will do the rest. The same applies to query parameters and the request body as well. Validation annotations are in place too so our basic requirements for these requests and their objects should be covered.

To have a bit more complex example let’s have a look at another case with nested objects and let the serializer do it’s magic. Basically, we want to filter our customer list and therefore we have our command

1 <?php 2 3 namespace App\Customer\Message\Query; 4 5 use Fusonic\HttpKernelExtensions\Dto\RequestDto; 6 7 final class GetCustomersQuery implements RequestDto 8 { 9 private CustomerFilter $filters; 10 11 public function __construct(?CustomerFilter $filters = null) 12 { 13 $this->filters = $filters ?? new CustomerFilter(); 14 } 15 16 public function getFilter(): CustomerFilter 17 { 18 return $this->filters; 19 } 20 21 public function setFilter(CustomerFilter $filters): void 22 { 23 $this->filters = $filters; 24 } 25 }

and this command has some optional filter options.

1 <?php 2 3 namespace App\Customer\Message\Query; 4 5 use Fusonic\HttpKernelExtensions\Dto\RequestDto; 6 7 final class GetCustomersQuery implements RequestDto 8 { 9 private CustomerFilter $filters; 10 11 public function __construct(?CustomerFilter $filters = null) 12 { 13 $this->filters = $filters ?? new CustomerFilter(); 14 } 15 16 public function getFilter(): CustomerFilter 17 { 18 return $this->filters; 19 } 20 21 public function setFilter(CustomerFilter $filters): void 22 { 23 $this->filters = $filters; 24 } 25 } 26 and this command has some optional filter options. 27 $this->lastName = $lastName; 28 } 29 }

Integrate it in the project

To make it work in our project the argument resolver has to be registered as service and tagged. That’s it. Then you have automatically created and validated objects in your controller actions.

1 Fusonic\HttpKernelExtensions\Controller\RequestDtoResolver: 2 tags: 3 - { name: controller.argument_value_resolver, priority: 50 }

Next step

In part 4 we are going to take a look at the whole messaging infrastructure and configure it to connect all the commands, queries and events with their handlers and have a fully working example.

Mehr davon?

Pragmatic architecture 4_CQRS-messaging_B
Dev Diary
Pragmatic architecture 4: CQRS and Messaging
4. März 2021 | 4 Min.
Pragmatic architecture 2_REST_B
Dev Diary
Pragmatic architecture 2: REST
2. März 2021 | 7 Min.

Kontaktformular

*Pflichtfeld
*Pflichtfeld
*Pflichtfeld
*Pflichtfeld

Wir schützen deine Daten

Wir bewahren deine persönlichen Daten sicher auf und geben sie nicht an Dritte weiter. Mehr dazu erfährst du in unseren Datenschutzbestimmungen.