Validating requests in the Symfony app
Tags: php, symfony, laravel
Published: 2021-10-26 15:00:34

> Starting with Symfony 6.3, such feature landed natively in the framework. Checkout this. https://symfony.com/blog/new-in-symfony-6-3-mapping-request-data-to-typed-objects

Hello 👋 

One night I was playing arround with the Symfony app & realized I don't like the act of validating the request body in the controller method itself. I am relatively new to Symfony, so I thought it might be a good thing to try myself & see if I can pull a cleaner way to do this.

Per docs, this is how it looks like:

```php
public function author(ValidatorInterface $validator)
{
    $author = new Author();

    // ... do something to the $author object

    $errors = $validator->validate($author);

    if (count($errors) > 0) {
        /*
         * Uses a __toString method on the $errors variable which is a
         * ConstraintViolationList object. This gives us a nice string
         * for debugging.
         */
        $errorsString = (string) $errors;

        return new Response($errorsString);
    }

    return new Response('The author is valid! Yes!');
}
```

This looks fine, as well, but I thought it might be nice if I can move this somewhere else.

Ideally, I should be able to just type-hint the request class and maybe call another method to perform validation.

This is how I imagined it. First, let's create the `ExampleRequest` class & define fields as just plain PHP properties.

```php
<?php

namespace App\Requests;

class ExampleRequest
{
    protected $id;

    protected $firstName;
}
```

Now, we can use PHP 8 attributes (or you can use annotations) to describe validation rules for the fields.

```php
<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class ExampleRequest
{
    #[Type('integer')]
    #[NotBlank()]
    protected $id;

    #[NotBlank([])]
    protected $firstName;
}
```

Perfect, now the fun part. Let's make the following API work:

```
#[Route('/app', name: 'app')]
public function index(ExampleRequest $request): Response
{
    $request->validate();

    return $this->json([
        'message' => 'Welcome to your new controller!',
        'path' => 'src/Controller/AppController.php',
    ]);
}
```

We don't have the `validate()` method on the `ExampleRequest`. Instead of adding it directly in there, I would create a `BaseRequest` class that can be re-used for all requests, so individual classes don't have to worry about validation, resolving, etc.


```php
<?php

namespace App\Requests;

use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
    }

    public function validate(): ConstraintViolationListInterface
    {
        return $this->validator->validate($this);
    }
}

```

> Don't forget to extend `BaseRequest` in `ExampleRequest` class.

If you run this, nothing related to validation is going to happen. You will see a regular controller response: Welcome to your new controller!

This is fine. We haven't told the app to stop on validation, break the request, or something else.

Let's see which errors, we got from the validator itself.

```php
#[Route('/app', name: 'app')]
public function index(ExampleRequest $request): Response
{
    $errors = $request->validate();

    dd($errors);

    return $this->json([
        'message' => 'Welcome to your new controller!',
        'path' => 'src/Controller/AppController.php',
    ]);
}
```

Let's fire request using these fields in the body.


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1634421963400/tDBrEQ_S2.png)

Hm, what is happening? We sent `id` in the request body, yet it still complains. **Well, we never mapped the request body to the ExampleRequest.**

Let's do that in the `BaseRequest` class.

```php
<?php

namespace App\Requests;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
        $this->populate();
    }

    public function validate(): ConstraintViolationListInterface
    {
        return $this->validator->validate($this);
    }

    public function getRequest(): Request
    {
        return Request::createFromGlobals();
    }

    protected function populate(): void
    {
        foreach ($this->getRequest()->toArray() as $property => $value) {
            if (property_exists($this, $property)) {
                $this->{$property} = $value;
            }
        }
    }
}
```

What we did here? First of all, we are calling the `populate()` method which will just loop through the request body & map the fields to the class properties, if that property exists.

If we fire the same request again, notice how the validation doesn't yell about the `id` property anymore.


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1634422275569/ceocpaLMs.png)

Let's provide `firstName` also and see what is going to happen.


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1634422325979/BxmFySzBI.png)

**Nice!** We passed the validator!

At this point, this is already an improvement since we don't have to call a validator on our own. But, let's take it a step further. Let's make it return the JSON with validation messages if something is wrong.

We want to refactor  `validate()` method in `BaseRequest`.

```php
public function validate()
{
    $errors = $this->validator->validate($this);

    $messages = ['message' => 'validation_failed', 'errors' => []];

    /** @var \Symfony\Component\Validator\ConstraintViolation  */
    foreach ($errors as $message) {
        $messages['errors'][] = [
            'property' => $message->getPropertyPath(),
            'value' => $message->getInvalidValue(),
            'message' => $message->getMessage(),
        ];
    }

    if (count($messages['errors']) > 0) {
        $response = new JsonResponse($messages);
        $response->send();

        exit;
    }
}
```

Woah, that's a huge change. It's pretty simple. First, we loop through validation messages & stack them into one massive array which will be the final response.

If we have validation errors at all, we gonna stop the current request & return the JSON response with all messages.

Let's remove the `dd()` from the controller & test it again.

```php
#[Route('/app', name: 'app')]
public function index(ExampleRequest $request): Response
{
    $request->validate();

    return $this->json([
        'message' => 'Welcome to your new controller!',
        'path' => 'src/Controller/AppController.php',
    ]);
}
```

.. now let's fire the request.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1634422612118/lgBvmVDCq.png)

Nice! That's cool, we are now automatically returning the validation messages. That's it! Now we can use plain PHP classes with attributes/annotations and validate nicely without having to call a validator each time on our own.

Bonus!
I wanted to remove that `$request->validate()` line as well. It's fairly simple!

We can automatically call the `validate()` method if, for example, we specify in the request class to automatically validate it.

Let's do it like this.

In `BaseRequest` add following method:

```php
protected function autoValidateRequest(): bool
{
    return true;
}
```

.. and now in the constructor of the same `BaseRequest` class, we can do the following:

```php
abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
        $this->populate();

        if ($this->autoValidateRequest()) {
            $this->validate();
        }
    }

   // Rest of BaseRequest
```

By default, we are going to validate the request & display the errors. If you want to disable this per request class, you can just overwrite this method.

```php
<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class ExampleRequest extends BaseRequest
{
    #[Type('integer')]
    #[NotBlank()]
    protected $id;

    #[NotBlank([])]
    protected $firstName;

    protected function autoValidateRequest(): bool
    {
        return false;
    }
}
```

Of course, you can adjust it to be `false` by default, your pick. 

Now we don't need to call `$request->validate()` at all. 
This is looking nice!

```php
    #[Route('/app', name: 'app')]
    public function index(ExampleRequest $request): Response
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/AppController.php',
        ]);
    }
```

This is BaseRequest after all changes:

```
<?php

namespace App\Requests;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class BaseRequest
{
    public function __construct(protected ValidatorInterface $validator)
    {
        $this->populate();
        
        if ($this->autoValidateRequest()) {
            $this->validate();
        }
    }

    public function validate()
    {
        $errors = $this->validator->validate($this);

        $messages = ['message' => 'validation_failed', 'errors' => []];

        /** @var \Symfony\Component\Validator\ConstraintViolation  */
        foreach ($errors as $message) {
            $messages['errors'][] = [
                'property' => $message->getPropertyPath(),
                'value' => $message->getInvalidValue(),
                'message' => $message->getMessage(),
            ];
        }

        if (count($messages['errors']) > 0) {
            $response = new JsonResponse($messages, 201);
            $response->send();

            exit;
        }
    }

    public function getRequest(): Request
    {
        return Request::createFromGlobals();
    }

    protected function populate(): void
    {
        foreach ($this->getRequest()->toArray() as $property => $value) {
            if (property_exists($this, $property)) {
                $this->{$property} = $value;
            }
        }
    }

    protected function autoValidateRequest(): bool
    {
        return true;
    }
}
```

.. and this is how to use it:

Step 1: Create request class & define properties. Annotate them with validation rules.

```php
<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class ExampleRequest
{
    #[Type('integer')]
    #[NotBlank()]
    protected $id;

    #[NotBlank([])]
    protected $firstName;
}
```

Step 2: Extend request class with `BaseRequest`

```php
class ExampleRequest extends BaseRequest
```

That's it! Happy coding!