Symfony Video Tutorial: Symfony 6.3: MapRequestPayload

During a LiveCoding session, one of the spectators made me discover a new feature of Symfony 6.3 that I had to share with you: MapRequestPayload & MapQueryString

This new feature automatically transforms the body of the request into typed objects (DTO) in your controllers (this is also a feature that already exists in other frameworks such as Laravel with FormRequests) and simplifies the logic.

In previous versions, if you wanted to read the data from the request, you had to decode it manually.

public function index (Request $request) {
    $data = json_deconde($request->getContent());
}

And we could rely on the Serializer to obtain a typed object, and the Validator to ensure the structure of the data.

public function index(
    Request $request,
    SerializerInterface $serializer,
    ValidatorInterface $validator,
): Response
{
    $data = $serializer->deserialize($request->getContent(), PaginationDTO::class, 'json');
    $errors = $validator->validate($data);
    if (count($errors) > 0) {
        // On traite les erreurs pour les afficher
        // ...
        return new JsonResponse([], Response::HTTP_UNPROCESSABLE_ENTITY);
    }
    // L'objet et valide on peut continuer
    // ...
    return new JsonResponse([]);
}

This was unfortunately a lot of code for a fairly common need, especially when working on an API.

MapRequestPayload & MapQueryString

Two new attributes have been added in version 6.3 of symfony to simplify this work. We start by creating the object that will represent our data.

<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class PaginationDTO
{

    public function __construct(
        #[Assert\Positive()]
        public readonly int $limit = 10,

        #[Assert\Positive()]
        public readonly int $page = 1,

        #[Assert\Valid]
        /**
         * @var FilterDTO[]
         */
        public readonly array $filters
    ){

    }

}

Then in our controller we can add a parameter typed with this class and we add the attribute MapRequestPayload if we want the object to be built from the query data and MapQueryString whether the object should be constructed from data coming from the URL.

<?php

namespace App\Controller;

use App\DTO\PaginationDTO;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Annotation\Route;

class DemoController extends AbstractController
{
    #[Route('/demo', name: 'app_demo')]
    public function index(
        #[MapRequestPayload] PaginationDTO $paginationDTO,
    ): Response
    {
        return new JsonResponse($paginationDTO);
    }
}

With this attribute Symfony will take care of building your object automatically from the data of the request and will validate that the object corresponds to your constraints. If the validation fails the error will be automatically serialized and returned as a response.

For more complex cases it is possible to customize the serialization context but also to change the resolver.

#[MapRequestPayload(
    serializationContext: ['...'],
    resolver: App\...\ProductReviewRequestValueResolver
)]

These 2 attributes will make it possible to greatly simplify the code of the controllers and make the work on the APIs much simpler.

If you want to discover more news about the 6.3 version of symfony, I refer you to the symfony blog article which lists the significant features of 6.3