DEV Community

Daniyal Javani
Daniyal Javani

Posted on • Edited on

A Simple Way to Validate API Requests in Symfony

Symfony is a popular PHP framework that provides many features to simplify web development. One of these features is the ability to validate API requests using data transfer objects (DTOs) and annotations. In this article, we will see how to use this feature to create a simple API endpoint for creating a post with some related fields.

What is a DTO?

A DTO is a class that represents the data that is transferred between different layers of an application, such as the presentation layer and the business layer. A DTO can be used to map the values from the request, validate them, and pass them to the service layer. A DTO can also be used to return the data from the service layer to the presentation layer.

How to create a DTO in Symfony?

To create a DTO in Symfony, we just need to create a class with some public properties that will hold the values from the request. We can also use annotations to define the validation rules for each property, such as NotBlank, Type, Range, etc. These annotations are provided by the Symfony Validator component, which is a powerful tool for validating data in Symfony.

Please ensure that all the required packages are installed before starting.install all necessary packages before starting:

composer require symfony/serializer
composer require symfony/validator
Enter fullscreen mode Exit fullscreen mode

For example, let's create a DTO class for creating a post with some related fields, such as title, content, author, category, and tags. We can use the following code to create the class:

<?php

namespace App\Dto;

use Symfony\Component\Validator\Constraints as Assert;

class CreatePostDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Type('string')]
        public readonly string $title,

        #[Assert\NotBlank]
        #[Assert\Type('string')]
        public readonly string $content,

        #[Assert\NotBlank]
        #[Assert\Type('string')]
        public readonly string $author,

        #[Assert\NotBlank]
        #[Assert\Type('string')]
        public readonly string $category,

        #[Assert\Type('array')]
        public ?array $tags = null,
    ) {
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we have defined the properties of the DTO class and used annotations to specify the validation rules for each property. We have also used the readonly modifier to make the properties immutable, which means they cannot be changed after the object is created. This is a good practice to ensure the consistency and integrity of the data.

How to use a DTO in a controller?

To use a DTO in a controller, we just need to use the class as a dependency in the method of the controller with the annotation #[MapRequestPayload]. This annotation tells Symfony to automatically map the values from the request into the object of the DTO class. We can also use the #[Route] annotation to define the route for the controller method.

For example, let's create a controller method for creating a post using the DTO class we created before. We can use the following code to create the method:

<?php

namespace App\Controller;

use App\Dto\CreatePostDto;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Annotation\Route;

class PostController extends AbstractController
{
    #[Route('/api/v1/posts', name: 'app_api_create_post_v1', methods: ['POST'])]
    public function v1Create(#[MapRequestPayload] CreatePostDto $createPost): JsonResponse
    {
        // Here we can use the $createPost object to pass the data to the service layer
        // For simplicity, we will just return the data as a JSON response
        return $this->json([
            'response' => 'ok',
            'title' => $createPost->title,
            'content' => $createPost->content,
            'author' => $createPost->author,
            'category' => $createPost->category,
            'tags' => $createPost->tags,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we have used the #[MapRequestPayload] annotation to inject the CreatePostDto object into the controller method. We have also used the #[Route] annotation to define the route for the method, which is /api/v1/posts with the POST method. Inside the method, we can use the $createPost object to access the data from the request and pass it to the service layer. For simplicity, we will just return the data as a JSON response using the json method of the AbstractController class.

How to test the API endpoint?

To test the API endpoint, we can use a tool like curl to send a request to the endpoint with the data we want to create a post. We need to make sure to add the Content-Type: application/json header to the request, as we are sending the data in JSON format. We can also use the -v option to see the verbose output of the request and the response.

For example, let's send a request to the endpoint with the following data:

{
  "title": null,
  "content": "In this article, we will see how to use DTOs and annotations to validate API request in Symfony",
  "author": "John Doe",
  "category": "PHP",
  "tags": ["Symfony", "API", "Validation"]
}
Enter fullscreen mode Exit fullscreen mode

We can use the following command to send the request:

curl -v --request POST \
  --url http://127.0.0.1:8001/api/v1/posts \
  --header 'Content-Type: application/json' \
  --data '{
  "title": null,
  "content": "In this article, we will see how to use DTOs and annotations to validate API request in Symfony",
  "author": "John Doe",
  "category": "PHP",
  "tags": ["Symfony", "API", "Validation"]
}'
Enter fullscreen mode Exit fullscreen mode

And here is the result:

< HTTP/1.1 422
< Content-Type: application/json

{
  "type": "https:\/\/symfony.com\/errors\/validation",
  "title": "Validation Failed",
  "status": 422,
  "detail": "This value should be of type unknown.\ntitle: This value should not be blank.",
  "violations": [
    {
      "propertyPath": "title",
      "title": "This value should not be blank.",
      "template": "This value should not be blank.",
      "parameters": {
        "{{ value }}": "null"
      },
      "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we have successfully created a post using the API endpoint and received the data back as a JSON response.

Conclusion

In this article, we have seen how to use DTOs and annotations to validate API request in Symfony. This is a simple and elegant way to handle the data from the request, validate it, and pass it to the service layer. We have also seen how to use the #[MapRequestPayload] annotation to automatically map the values from the request into the object of the DTO class. We have also seen how to test the API endpoint using curl.

We hope you have enjoyed this article and learned something new. If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading!

Top comments (2)

Collapse
 
woland0203 profile image
woland0203

Thanks, useful post.

In my opinion, it would be useful to expand the article with examples when the response is not json, but an html page. Where should a developer catch a validation exception and how to display a custom HTML error page?

Collapse
 
hamhamfonfon profile image
Stéphane Méaudre

Can we test the input DTO integrity with PHP Unit ?