
Symfony framework starting from version 4 and beyond, has offered a set of components that have proven to be highly effective and invaluable.
I’ve been a heavy user of Symfony components throughout my career. However, one issue that has particularly bothered me is the inability to directly map request payloads to a Data Transfer Object (DTO) representing our requests in controllers or services without the need for manual mapping, using the Symfony serializer component.
Fortunately for the PHP community, Symfony has introduced a new attribute #[MapRequestPayload], which facilitates the mapping of our requests to a Data Transfer Object.
In this article we will discover, how to properly handle requests using this new feature.
🚀 Modeling requests through Data Transfer Object
Supposed we want to map the following POST request:
{
"name": "mmouih",
"address": {
"address": "1 sesamy street",
"zipcode": "75000",
"city": "Paris",
"country": "France",
"geolocalisation": {
"lng": 2.349014,
"lat": 48.864716
}
}
}In order to model our request, we will a DTO, a nested POPO (Plain old Php object).
We are leveraging Symfony Validator component to ensure that our request is valid.
<?php
namespace App\Payload;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Type;
readonly class User
{
public function __construct(
#[Length(min: 3)]
#[Type(type: 'alpha')]
public string $name,
#[Assert\Valid]
public ?Address $address = null,
) {
}
}
<?php
namespace App\Payload;
readonly class Address
{
public function __construct(
public string $address,
public string $zipcode,
public string $city,
public string $country,
public Geolocalisation $geolocalisation,
) {
}
}
<?php
namespace App\Payload;
readonly class Geolocalisation
{
public function __construct(
public float $lat,
public float $lng,
) {
}
}
🚀 The controller & MapRequestPayload
Using dependency injection & MapRequestPayload, we are going to inject our Dto to the controller argument.
<?php
namespace App\Controller;
use App\Payload\User;
use App\Handler\CreateOrUpdateUserHandler;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
class UserController
{
#[Route('/user', name: 'create_or_update_user', methods: ['POST'])]
public function createOrUpdate(
#[MapRequestPayload] User $user,
CreateOrUpdateUserHandler $handler
): JsonResponse {
$handler->handle($user);
return new JsonResponse(['message' => sprintf('User %s has been created', $user->name)]);
}
}
Currently we have our validated DTO injected in the controller, perfect 🤗. we have only have to apply our logic and business requirement. I personally prefer using handlers to apply business logic to my DTO.
After implementing business logic, we create and return a response message based on our specific requirements.

🚨 Handling validation errors
Until now, we only discovered the happy path, next we will dive into the dark side. What to do if an error occurred.
Consider the following payload:
{
"name": "mmouih23",
"address": {
"address": "1 sesamy street",
"zipcode": "75000",
"city": "Paris",
"country": "France",
"geolocalisation": {
"lng": "2.349014",
"lat": 8.864716
}
}
}This payload is invalid, because the name contains digits. and geolocalisation.lng is not an integer.
Our payload mapper Resolver ( RequestPayloadValueResolver by default will through an http exception)

This exception provide us with information. but it lacks specificity regarding the invalid values in the schema. Additionally, We may need a formatted JSON response, to provide for our service consumers like a front-end application.
Luckily, Symfony provide us with Event dispatcher component, which a core concept in Symfony framework, In encourage you to read https://symfony.com/doc/current/components/event_dispatcher.html for more information.
Using Event dispatcher, we have the ability to subscribe to various events, including core events within the framework, such as the ExceptionEvent, for example.
Then we only have to verify the context of our exception, to make sure it is a payload validation from an http request. We can format our response according to our needs.
PayloadErrorEventSubscriber.php
<?php
namespace App\EventSubscriber;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
class PayloadErrorEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ExceptionEvent::class => 'onExceptionEvent'
];
}
public function onExceptionEvent(ExceptionEvent $event): void
{
$isHttpEvent = $event->getThrowable() instanceof HttpExceptionInterface;
$isValidationEvent = $event->getThrowable()->getPrevious() instanceof ValidationFailedException;
// We are only interested in validation errors in an httpException context
if (!$isHttpEvent || !$isValidationEvent) {
return;
}
/**
* @var ValidationFailedException $validationException
*/
$validationException = $event->getThrowable()->getPrevious();
$errorMessages = [];
foreach ($validationException->getViolations() as $violation) {
$errorMessages[$violation->getPropertyPath()] = $violation->getMessage();
}
$event->setResponse(new JsonResponse(['errors' => $errorMessages], Response::HTTP_UNPROCESSABLE_ENTITY));
}
}
Are a result are obtain the following response :
{
"errors": {
"address.geolocalisation.lng": "This value should be of type float.",
"name": "This value should be of type alpha."
}
}The response contains everything we need to display a proper error to our service consumers 🤗.
Reusibility Other Frameworks like Laravel
Symfony components are reusable by design, making it easy to integrate Symfony features into other frameworks like Laravel. Laravel integrates multiple Symfony components into its core, so it wouldn’t be surprising to find this feature integrated into upcoming major Laravel versions. We may dive deeper into this topic at a later point.
Thanks for reading, consider clapping and sharing 🔗 if you like the content.