Home / Symfony / New in Symfony 8.1: Improved Request Payload Mapping

New in Symfony 8.1: Improved Request Payload Mapping

Symfony controllers can map request data directly into typed PHP objects
using attributes such as #[MapRequestPayload] and #[MapQueryString].
This removes most of the boilerplate involved in request parsing and validation.
Symfony 8.1 further improves this feature with four new additions.

Mapping Uploaded Files into DTOs


Jonathan
Contributed by
Jonathan
in
#62925

Until now, DTOs containing both scalar fields and uploaded files could not be
populated through #[MapRequestPayload]. Developers had to either split the
controller into separate argument resolvers or manually merge $request->files
into the data array before deserialization.

In Symfony 8.1, #[MapRequestPayload] handles multipart/form-data requests
by merging request parameters and uploaded files, including nested arrays, before
deserialization. This means that an UploadedFile property is populated transparently:

use SymfonyComponentHttpFoundationFileUploadedFile;

class ProductDto
{
    public ?string $name = null;
    public ?UploadedFile $image = null;
}

class ProductController
{
    public function upload(
        #[MapRequestPayload] ProductDto $data,
    ): Response {
        // $data->name comes from the form fields
        // $data->image is an UploadedFile instance
    }
}

Variadic Controller Arguments


Djordy Koert
Contributed by
Djordy Koert
in
#54817

Mapping an array of DTOs previously required a typed array parameter together
with the type option. Symfony 8.1 adds support for variadic parameters,
which is more idiomatic in PHP:

class ProductController
{
    public function createPrices(
        #[MapRequestPayload] Price ...$prices,
    ): Response {
        foreach ($prices as $price) {
            // ...
        }
    }
}

With a JSON payload such as [{"value": 50}, {"value": 23}], $prices is
unpacked into a sequence of Price instances. The same syntax also works with
#[MapQueryString] and #[MapUploadedFile].

Mapping Empty Payloads


Jeroen Spee
Contributed by
Jeroen Spee
in
#52134

By default, an empty query string or request body short-circuits to null
on a nullable parameter without invoking the Serializer. This prevents custom
denormalizers from injecting values from the security context, session, or
other sources into the DTO.

Symfony 8.1 introduces a mapWhenEmpty option for #[MapRequestPayload]
and #[MapQueryString]. When set to true, denormalization runs even on
empty input. In this case, the denormalizer receives an empty array, giving
custom denormalizers a chance to populate the DTO:

class SearchController
{
    public function search(
        #[MapQueryString(mapWhenEmpty: true)] SearchFilters $filters,
    ): Response {
        // denormalization runs even with an empty query string,
        // so a custom denormalizer can e.g. inject $filters->userId
        // from the security context
    }
}

Dynamic Validation Groups


Adrian Brajković
Contributed by
Adrian Brajković
in
#58273

The validationGroups option of #[MapRequestPayload] and #[MapQueryString]
previously accepted only static strings. As soon as validation groups depended on
resolved request arguments, such as applying stricter rules for admins than for
regular users, you had to bypass the mapper and call the validator manually.

Symfony 8.1 now allows passing an Expression or a Closure to
validationGroups. Both are evaluated at validation time, and an args
variable provides access to all resolved controller arguments:

use SymfonyComponentExpressionLanguageExpression;
use SymfonyComponentHttpKernelAttributeMapRequestPayload;
use SymfonyComponentRoutingAttributeRoute;

class UserController
{
    #[Route('/users/{user}', methods: ['PUT'])]
    public function update(
        User $user,
        #[MapRequestPayload(
            validationGroups: [new Expression('args["user"].getType()')],
        )] UpdateUserDto $dto,
    ): Response {
        // Validation groups are computed from the resolved $user
    }
}

A Closure works the same way and receives the controller arguments as a
single array. This is useful when the logic does not fit on a single line.


Sponsor the Symfony project.
Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *