A Simple Way to Validate API Requests in Symfony
Tags: symfony, php, validate
Published: 2023-11-29 11:43:06

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](https://symfony.com/doc/current/validation.html), 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:

```bash
composer require symfony/serializer
composer require symfony/validator
```

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
<?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,
    ) {
    }
}
```

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
<?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,
        ]);
    }
}
```

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:

```json
{
  "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"]
}
```

We can use the following command to send the request:

```bash
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"]
}'
```

And here is the result:

```json
< 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"
    }
  ]
}
```

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!