Konstantin Myakshin
Contributed by Konstantin Myakshin in #49138

A recurring Symfony feature request during the past years has been the mapping of the incoming request data into typed objects like DTO (data transfer objects). In Symfony 6.3 we're finally introducing some new attributes to map requests to typed objects and validate them.

First, the #[MapRequestPayload] attribute takes the data from the $_POST PHP superglobal (via the $request->request->all() method of the Symfony Request object) and tries to populate a given typed object with it.

Consider the following DTO class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
use Symfony\Component\Validator\Constraints as Assert;

class ProductReviewDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 10, max: 500)]
        public readonly string $comment,

        #[Assert\GreaterThanOrEqual(1)]
        #[Assert\LessThanOrEqual(5)]
        public readonly int $rating,
    ) {
    }
}

In Symfony 6.3, use that class as the type-hint of some controller argument and apply the #[MapRequestPayload] attribute. Symfony will map the request data into the DTO object automatically and will validate it:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;

class ProductApiController
{
    public function __invoke(
        #[MapRequestPayload] ProductReviewDto $productReview,
    ): Response {

        // here, $productReview is a fully typed representation of the request data

    }
}

That's all. About the possible error conditions when mapping data:

  • Validation errors will result in HTTP 422 error responses (including a serialized ConstraintViolationList object);
  • Malformed data will be responded to with HTTP 400 error responses;
  • Unsupported deserialization formats will be responded to with HTTP 415 error responses.

Similarly, the #[MapQueryString] takes the data from the $_GET PHP superglobal (via the $request->query->all() method of the Symfony Request object) and tries to populate a given typed object with it.

Consider the following set of DTO classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ...
use Symfony\Component\Validator\Constraints as Assert;

class OrdersQueryDto
{
    public function __construct(
        #[Assert\Valid]
        public readonly ?OrdersFilterDto $filter,

        #[Assert\LessThanOrEqual(500)]
        public readonly int $limit = 25,

        #[Assert\LessThanOrEqual(10_000)]
        public readonly int $offset = 0,
    ) {
    }
}

class OrdersFilterDto
{
    public function __construct(
        #[Assert\Choice(['placed', 'shipped', 'delivered'])]
        public readonly ?string $status,

        public readonly ?float $total,
    ) {
    }
}

In Symfony 6.3, use that class as the type-hint of some controller argument and apply the #[MapQueryString] attribute. Symfony will map the request data into the DTO object automatically and will validate it:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
use Symfony\Component\HttpKernel\Attribute\MapQueryString;

class SearchApiController
{
    public function __invoke(
        #[MapQueryString] OrdersQueryDto $query,
    ): Response {

        // here, $query is a fully typed representation of the request data

    }
}

The validation logic and the error conditions of this attribute are the same as before. Also, the two attributes allow to customize both the serialization context and the class used to map the request to your objects:

1
2
3
4
5
6
7
8
9
10
11
#[MapRequestPayload(
    serializationContext: ['...'],
    resolver: App\...\ProductReviewRequestValueResolver
)]
ProductReviewDto $productReview

#[MapQueryString(
    serializationContext: ['...'],
    resolver: App\...\OrderSearchRequestValueResolver
)]
OrdersQueryDto $query
Published in #Living on the edge