Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Validating requests on Symfony Framework
Joubert RedRat
Joubert RedRat

Posted on • Edited on • Originally published atblog.redrat.com.br

     

Validating requests on Symfony Framework

TodaySymfony is one of the most mature PHP frameworks in the world and because of this, it's used in various projects, including the APIs creation. Recently Symfony included various cool features, likemapping request data to typed objects, that appeared in version 6.3.

With this, we will take advantage of some of the best resources from the last versions of PHP, that provide support toattributes andreadonly properties and create validations for Requests in Symfony.

For this, We will use theSymfony Validation component.

I have no patience, show me the code!

Okay okay! If you aren't patient to read this post, I have a test project with this implementation of this post in the link below.

https://github.com/joubertredrat/symfony-request-validation

Basic example

Following the Symfony documentation, we just need to create a class that will be used for mapping the values from the request, as example below.

<?phpdeclare(strict_types=1);namespaceApp\Dto;useApp\Validator\CreditCard;useSymfony\Component\Validator\Constraints\NotBlank;useSymfony\Component\Validator\Constraints\Positive;useSymfony\Component\Validator\Constraints\Range;useSymfony\Component\Validator\Constraints\Type;classCreateTransactionDto{publicfunction__construct(#[NotBlank(message: 'I dont like this field empty')]#[Type('string')]publicreadonlystring$firstName,#[NotBlank(message: 'I dont like this field empty')]#[Type('string')]publicreadonlystring$lastName,#[NotBlank()]#[Type('string')]#[CreditCard()]publicreadonlystring$cardNumber,#[NotBlank()]#[Positive()]publicreadonlyint$amount,#[NotBlank()]#[Type('int')]#[Range(min:1,max:12,notInRangeMessage:'Expected to be between {{ min }} and {{ max }}, got {{ value }}',)]publicreadonlyint$installments,#[Type('string')]public?string$description=null,){}}
Enter fullscreen modeExit fullscreen mode

With this, we just use the class as a dependency in the method of the controller with the annotation#[MapRequestPayload] and that's it, the values will be automatically mapped into the object, like the example below.

<?phpdeclare(strict_types=1);namespaceApp\Controller;useApp\Dto\CreateTransactionDto;useDateTimeImmutable;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\HttpFoundation\JsonResponse;useSymfony\Component\HttpKernel\Attribute\MapRequestPayload;useSymfony\Component\Routing\Annotation\Route;classTransactionControllerextendsAbstractController{#[Route('/api/v1/transactions', name: 'app_api_create_transaction_v1', methods: ['POST'])]publicfunctionv1Create(#[MapRequestPayload]CreateTransactionDto$createTransaction):JsonResponse{return$this->json(['response'=>'ok','datetime'=>(newDateTimeImmutable('now'))->format('Y-m-d H:i:s'),'firstName'=>$createTransaction->firstName,'lastName'=>$createTransaction->lastName,'amount'=>$createTransaction->amount,'installments'=>$createTransaction->installments,'description'=>$createTransaction->description,]);}}
Enter fullscreen modeExit fullscreen mode

With this, just do the request and check the result.

curl--request POST\--url http://127.0.0.1:8001/api/v1/transactions\--header'Content-Type: application/json'\--data'{  "firstName": "Joubert",  "lastName": "RedRat",  "cardNumber": "4130731304267489",  "amount": 35011757,  "installments": 2}'
Enter fullscreen modeExit fullscreen mode
<HTTP/1.1200OK<Content-Type:application/json{"response":"ok","datetime":"2023-07-04 19:36:37","firstName":"Joubert","lastName":"RedRat","cardNumber":"4130731304267489","amount":35011757,"installments":2,"description":null}
Enter fullscreen modeExit fullscreen mode

In the example above, if the values aren't correctly filled as the rules defined, will throw an exception and we will receive a response with the found errors.

Request error exception

The problem is this exception is the defaultValidationFailedException but as we are building an API, it's necessary a response in JSON format.

With this in mind, we can try a different approach, which will be explained ahead.

UPDATE August/2023: After publishing this post and sharing it on Symfony's Slack.Faizan andmdeboer told me that it's possible to have a JSON response because Symfony has a normalizer for these cases.

To be able to get a JSON response, you should add a headerAccept: application/json, with this you will get a JSON response, like the example below.

Symfony exception response json

As you can view, the response is in JSON, but, in the Symfony layout based on RFC 7807. In the next steps, we will do a class in which we can format a JSON response in the layout that we want.

ThanksFaizan andmdeboer for the contribution.

Abstract Request class

One of the biggest advantages of Symfony is the big and extensive support to DIP "Dependency inversion principle" by your powerful dependency injection container with autowire support.

With this, we will create our abstract class, which will have all code responsible to do the parse of request and validation, as in the example below.

<?phpdeclare(strict_types=1);namespaceApp\Request;useFig\Http\Message\StatusCodeInterface;useJawira\CaseConverter\Convert;useSymfony\Component\HttpFoundation\JsonResponse;useSymfony\Component\HttpFoundation\Request;useSymfony\Component\HttpFoundation\RequestStack;useSymfony\Component\Validator\Validator\ValidatorInterface;abstractclassAbstractJsonRequest{publicfunction__construct(protectedValidatorInterface$validator,protectedRequestStack$requestStack,){$this->populate();$this->validate();}publicfunctiongetRequest():Request{return$this->requestStack->getCurrentRequest();}protectedfunctionpopulate():void{$request=$this->getRequest();$reflection=new\ReflectionClass($this);foreach($request->toArray()as$property=>$value){$attribute=self::camelCase($property);if(property_exists($this,$attribute)){$reflectionProperty=$reflection->getProperty($attribute);$reflectionProperty->setValue($this,$value);}}}protectedfunctionvalidate():void{$violations=$this->validator->validate($this);if(count($violations)<1){return;}$errors=[];/** @var \Symfony\Component\Validator\ConstraintViolation */foreach($violationsas$violation){$attribute=self::snakeCase($violation->getPropertyPath());$errors[]=['property'=>$attribute,'value'=>$violation->getInvalidValue(),'message'=>$violation->getMessage(),];}$response=newJsonResponse(['errors'=>$messages],400);$response->send();exit;}privatestaticfunctioncamelCase(string$attribute):string{return(newConvert($attribute))->toCamel();}privatestaticfunctionsnakeCase(string$attribute):string{return(newConvert($attribute))->toSnake();}}
Enter fullscreen modeExit fullscreen mode

In the class above it's possible to see theValidatorInterface and theRequestStack as the dependencies and in the constructor is executed the fill and validation of attributes.

Also, it's possible to see the conversion between the stylesnake_case andcamelCase in the attributes and errors, this happens because exists a convention that the fields in the JSON must besnake_case, andPSR-2 andPSR-12 suggest thecamelCase for the names of attributes in the classes, then it's necessary a conversion. For this was used theCase converter lib.

But, it's good to remember it isn't an absolute rule, if you want to use a different default fromsnake_case in the JSON, you can.

Request Class with validation attributes

With an abstract class responsible for all validation, now we will create validation classes, as an example below.

<?phpdeclare(strict_types=1);namespaceApp\Request;useApp\Validator\CreditCard;useSymfony\Component\Validator\Constraints\NotBlank;useSymfony\Component\Validator\Constraints\Positive;useSymfony\Component\Validator\Constraints\Range;useSymfony\Component\Validator\Constraints\Type;classCreateTransactionRequestextendsAbstractJsonRequest{#[NotBlank(message: 'I dont like this field empty')]#[Type('string')]publicreadonlystring$firstName;#[NotBlank(message: 'I dont like this field empty')]#[Type('string')]publicreadonlystring$lastName;#[NotBlank()]#[Type('string')]#[CreditCard()]publicreadonlystring$cardNumber;#[NotBlank()]#[Positive()]publicreadonlyint$amount;#[NotBlank()]#[Type('int')]#[Range(min:1,max:12,notInRangeMessage:'Expected to be between {{ min }} and {{ max }}, got {{ value }}',)]publicreadonlyint$installments;#[Type('string')]public?string$description=null;}
Enter fullscreen modeExit fullscreen mode

The big advantage of the class above is that all required attributes of request are withreadonly status, being possible to warranty the immutability of the data.

Another interesting point is to be able to use attributes of Symfony Validation for doing the necessary validations or even create customized validations.

Using request class in Route

With the request class ready, now it's just to use as a dependency in the route that we want to do the validation, and it's good to remember that different from the previous example, here isn't necessary#[MapRequestPayload] annotation, as an example below.

<?phpdeclare(strict_types=1);namespaceApp\Controller;useApp\Request\CreateTransactionRequest;useDateTimeImmutable;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\HttpFoundation\JsonResponse;useSymfony\Component\Routing\Annotation\Route;classTransactionControllerextendsAbstractController{#[Route('/api/v2/transactions', name: 'app_api_create_transaction_v2', methods: ['POST'])]publicfunctionv2Create(CreateTransactionRequest$request):JsonResponse{return$this->json(['response'=>'ok','datetime'=>(newDateTimeImmutable('now'))->format('Y-m-d H:i:s'),'first_name'=>$request->firstName,'last_name'=>$request->lastName,'amount'=>$request->amount,'installments'=>$request->installments,'description'=>$request->description,'headers'=>['Content-Type'=>$request->getRequest()->headers->get('Content-Type'),],]);}}
Enter fullscreen modeExit fullscreen mode

In the controller above, it's possible to see that we didn't use the traditionalRequest from HttpFoundation, instead of this, our classCreateTransactionRequest as dependency and it's in that magic happens because all class dependencies are injected and validation is done.

Advantages of this approach

In comparison with the basic example, this approach has 2 advantages.

  • It's possible to customize the JSON structure and status code of the response as you want.

  • It's possible to have access toRequest class from Symfony which was injected as a dependency, with this it's possible to access any information from a request, as headers for example. In the basic example, this isn't possible unless you also addRequest class as a dependency in route, which sounds strange, because it has two distinct sources of data in the same request.

It's test time!

With our implementation ready, let's go for the tests.

The request example is with deliberate errors to be able to us see the validation response.

curl--request POST\--url http://127.0.0.1:8001/api/v2/transactions\--header'Content-Type: application/json'\--data'{  "last_name": "RedRat",  "card_number": "1130731304267489",  "amount": -4,  "installments": 16}'
Enter fullscreen modeExit fullscreen mode
<HTTP/1.1400BadRequest<Content-Type:application/json{"errors":[{"property":"first_name","value":null,"message":"I dont like this field empty."},{"property":"card_number","value":"1130731304267489","message":"Expected valid credit card number."},{"property":"amount","value":-4,"message":"This value should be positive."},{"property":"installments","value":16,"message":"Expected to be between 1 and 12, got 16"}]}
Enter fullscreen modeExit fullscreen mode

As we can see, the validation happened with success and fields not filled or with wrong data didn't pass in validation and we got a validation response.

Now, let's do a valid request and see that will have success in response because all fields will be filled as we want.

curl--request POST\--url http://127.0.0.1:8001/api/v2/transactions\--header'Content-Type: application/json'\--data'{  "first_name": "Joubert",  "last_name": "RedRat",  "card_number": "4130731304267489",  "amount": 35011757,  "installments": 2}'
Enter fullscreen modeExit fullscreen mode
<HTTP/1.1200OK<Content-Type:application/json{"response":"ok","datetime":"2023-07-01 16:39:48","first_name":"Joubert","last_name":"RedRat","card_number":"4130731304267489","amount":35011757,"installments":2,"description":null}
Enter fullscreen modeExit fullscreen mode

Limitations

The optional fields can't bereadonly because if you want to access the data without initialization, PHP will throw an exception. Then, for now, I'm using normal fields with default values for this case.

I'm still looking for any option as a solution for being able to usereadonly in optional fields, like using Reflection for example, and I accept suggestions.

Exception readonly attribute not initialized

Finally, I let my gratitude to my great friendVinicius Dias who helped me with the revision of this post.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software Engineer, DevOps, Cosplayer, Speaker, Opensource community evangelist
  • Location
    Belo Horizonte, MG, Brazil
  • Work
    Senior Software Engineer
  • Joined

More fromJoubert RedRat

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp