New in Symfony 6.3: Mapping Request Data to Typed Objects


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:

// ...
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:

// ...
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:

// ...
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:

// ...
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:

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

#[MapQueryString(
serializationContext: ['...'],
resolver: App\...\OrderSearchRequestValueResolver
)]
OrdersQueryDto $query

Sponsor the Symfony project.