Maison > Article > développement back-end > Un moyen simple de valider les DTO à l'aide des attributs Symfony
Les DTO sont des objets simples qui encapsulent des attributs de données sans contenir aucune logique métier. Ils sont souvent utilisés pour regrouper des données provenant de plusieurs sources en un seul objet, ce qui facilite leur gestion et leur transmission. En utilisant les DTO, les développeurs peuvent réduire le nombre d'appels de méthodes, améliorer les performances et simplifier la gestion des données, en particulier dans les systèmes distribués ou les API.
A titre d'exemple, nous pouvons utiliser des DTO pour mapper les données reçues via une requête HTTP. Ces DTO conserveraient dans leurs propriétés les valeurs de charge utile reçues et nous pourrions les utiliser dans l'application, par exemple, en créant un objet d'entité de doctrine prêt à être conservé dans la base de données à partir des données contenues dans le DTO. Comme les données DTO seraient déjà validées, cela peut réduire la probabilité de générer des erreurs lors de la persistance de la base de données.
Les attributs MapQueryString et MapRequestPayload nous permettent de mapper respectivement la chaîne de requête reçue et les paramètres de charge utile. Voyons un exemple des deux.
Imaginons que nous ayons une route Symfony qui puisse recevoir les paramètres suivants dans la chaîne de requête :
Sur la base des paramètres ci-dessus, nous souhaitons les mapper au dto suivant :
readonly class QueryInputDto { public function __construct( #[Assert\Datetime(message: 'From must be a valid datetime')] #[Assert\NotBlank(message: 'From date cannot be empty')] public string $from, #[Assert\Datetime(message: 'To must be a valid datetime')] #[Assert\NotBlank(message: 'To date cannot be empty')] public string $to, public ?int $age = null ){} }
Pour les mapper, il suffit d'utiliser l'attribut MapQueryString de cette manière :
#[Route('/my-get-route', name: 'my_route_name', methods: ['GET'])] public function myAction(#[MapQueryString] QueryInputDTO $queryInputDto) { // your code }
Comme vous pouvez le voir, lorsque symfony détecte que l'argument $queryInputDto a été marqué avec l'attribut #[MapQueryString], il mappe automatiquement les paramètres de la chaîne de requête reçus dans cet argument qui est une instance du Classe QueryInputDTO.
Dans ce cas, imaginons que nous ayons une route Symfony qui reçoit les données requises pour enregistrer un nouvel utilisateur dans la charge utile de la requête JSON. Ces paramètres sont les suivants :
Sur la base des paramètres ci-dessus, nous souhaitons les mapper au dto suivant :
readonly class PayloadInputDto { public function __construct( #[Assert\NotBlank(message: 'Name cannot be empty')] public string $name, #[Assert\NotBlank(message: 'Email cannot be empty')] #[Assert\Email(message: 'Email must be a valid email')] public string $email, #[Assert\NotBlank(message: 'From date cannot be empty')] #[Assert\Date(message: 'Dob must be a valid date')] public ?string $dob = null ){} }
Pour les cartographier, il suffit d'utiliser l'attribut MapRequestPayload de cette manière :
#[Route('/my-post-route', name: 'my_post_route', methods: ['POST'])] public function myAction(#[MapRequestPayload] PayloadInputDTO $payloadInputDto) { // your code }
Comme nous l'avons vu dans la section précédente, lorsque symfony détecte que l'argument $payloadInputDto a été marqué avec l'attribut #[MapRequestPayload], il mappe automatiquement les paramètres de charge utile reçus dans cet argument qui est une instance de la classe PayloadInputDTO.
MapRequestPayload fonctionne à la fois pour les charges utiles JSON et les charges utiles codées par URL de formulaire.
Si la validation échoue pendant le processus de mappage (par exemple, l'e-mail obligatoire n'a pas été envoyé), Symfony renvoie une exception 422 Unprocessable Content. Si vous souhaitez détecter ce type d'exceptions et renvoyer les erreurs de validation comme, par exemple, json au client, vous pouvez créer un abonné à l'événement et continuer à écouter l'événement KernelException.
class KernelSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ KernelEvents::EXCEPTION => 'onException' ]; } public function onException(ExceptionEvent $event): void { $exception = $event->getThrowable(); if($exception instanceof UnprocessableEntityHttpException) { $previous = $exception->getPrevious(); if($previous instanceof ValidationFailedException) { $errors = []; foreach($previous->getViolations() as $violation) { $errors[] = [ 'path' => $violation->getPropertyPath(), 'error' => $violation->getMessage() ]; } $event->setResponse(new JsonResponse($errors)); } } } }
Une fois la méthode onException déclenchée, elle vérifie si l'exception d'événement est une instance de UnprocessableEntityHttpException. Si tel est le cas, il vérifie également si l'erreur non traitable provient d'un échec de validation vérifiant si l'exception précédente est une instance de la classe ValidationFailedException. Si tel est le cas, il stocke toutes les erreurs de violation dans un tableau (uniquement le chemin de la propriété comme clé et le message de violation comme erreur), crée une réponse JSON à partir de ces erreurs et définit la nouvelle réponse à l'événement.
L'image suivante montre la réponse JSON pour une requête qui échoue car l'e-mail n'a pas été envoyé :
@baseUrl = http://127.0.0.1:8000 POST {{baseUrl}}/my-post-route Content-Type: application/json { "name" : "Peter Parker", "email" : "", "dob" : "1990-06-28" } ------------------------------------------------------------- HTTP/1.1 422 Unprocessable Entity Cache-Control: no-cache, private Content-Type: application/json Date: Fri, 20 Sep 2024 16:44:20 GMT Host: 127.0.0.1:8000 X-Powered-By: PHP/8.2.23 X-Robots-Tag: noindex Transfer-Encoding: chunked [ { "path": "email", "error": "Email cannot be empty" } ]
La demande d'image ci-dessus a été générée à l'aide de fichiers http.
Imaginons que nous ayons des routes qui reçoivent les paramètres de la chaîne de requête dans un tableau nommé "f". Quelque chose comme ça :
/my-get-route?f[from]=2024-08-20 16:24:08&f[to]=&f[age]=14
Nous pourrions créer un résolveur personnalisé pour vérifier ce tableau dans la requête, puis valider les données. Codons-le.
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\Validator\ValidatorInterface; class CustomQsValueResolver implements ValueResolverInterface, EventSubscriberInterface { public function __construct( private readonly ValidatorInterface $validator, private readonly SerializerInterface $serializer ){} public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments', ]; } public function resolve(Request $request, ArgumentMetadata $argument): iterable { $attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null; if (!$attribute) { return []; } if ($argument->isVariadic()) { throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())); } $attribute->metadata = $argument; return [$attribute]; } public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { $arguments = $event->getArguments(); $request = $event->getRequest(); foreach ($arguments as $i => $argument) { if($argument instanceof MapQueryString ) { $qs = $request->get('f', []); if(count($qs) > 0) { $object = $this->serializer->denormalize($qs, $argument->metadata->getType()); $violations = $this->validator->validate($object); if($violations->count() > 0) { $validationFailedException = new ValidationFailedException(null, $violations); throw new UnprocessableEntityHttpException('Unale to process received data', $validationFailedException); } $arguments[$i] = $object; } } } $event->setArguments($arguments); } }
The CustomQsValueResolver implements the ValueResolverInterface and the EventSubscriberInterface, allowing it to resolve arguments for controller actions and listen to specific events in the Symfony event system. In this case, the resolver listens to the Kernel CONTROLLER_ARGUMENTS event.
Let's analyze it step by step:
The constructor takes two dependencies: The Validator service for validating objects and a the Serializer service for denormalizing data into objects.
The getSubscribedEvents method returns an array mapping the KernelEvents::CONTROLLER_ARGUMENTS symfony event to the onKernelControllerArguments method. This means that when the CONTROLLER_ARGUMENTS event is triggered (always a controller is reached), the onKernelControllerArguments method will be called.
The resolve method is responsible for resolving the value of an argument based on the request and its metadata.
The onKernelControllerArguments method is called when the CONTROLLER_ARGUMENTS event is triggered.
To instruct the MapQueryString attribute to use our recently created resolver instead of the default one, we must specify it with the attribute resolver value. Let's see how to do it:
#[Route('/my-get-route', name: 'my_route_name', methods: ['GET'])] public function myAction(#[MapQueryString(resolver: CustomQsValueResolver::class)] QueryInputDTO $queryInputDto) { // your code }
In this article, we have analized how symfony makes our lives easier by making common application tasks very simple, such as receiving and validating data from an API. To do that, it offers us the MapQueryString and MapRequestPayload attributes. In addition, it also offers us the possibility of creating our custom mapping resolvers for cases that require specific needs.
If you like my content and enjoy reading it and you are interested in learning more about PHP, you can read my ebook about how to create an operation-oriented API using PHP and the Symfony Framework. You can find it here: Building an Operation-Oriented Api using PHP and the Symfony Framework: A step-by-step guide
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!