Data sent to the server always have to be validated. For that, validation should be applied for every single request that comes to server to aware about data which we expect to have in request. As data needs to be validated before it is written into a database or passed to a web service, we should validate request before it comes to Controller.

In this article, we will share with you how we implemented the validation of request in the Symfony, in the likeness of Laravel FormRequest. To validate data from request we will have a separate class which is an extra level over the original request, but with functionalities to validate incoming data and throw an exception, if data in the request is invalid.

Accessing request properties and methods

In order to give the validator the ability to validate our data, we will merge them and make them accessible through method all(). All other properties of the original request will also be available to us. For that we can implement the following trait:

Code example

<?php

namespace App\Traits;

use Symfony\Component\HttpFoundation\Request;

trait ValidationAwareTrait
{
    /** @var Request */
    protected $httpRequest;

    public function __get($property)
    {
        if (property_exists($this->httpRequest, $property)) {
            return $this->httpRequest->$property;
        }

        $trace = debug_backtrace();
        $fileName = $trace[0]['file'] ?? __FILE__;
        $line = $trace[0]['line'] ?? __LINE__;
        trigger_error("Undefined property {$property} in {$fileName} on line {$line}");

        return null;
    }

    public function __set($property, $value)
    {
        if (property_exists($this->httpRequest, $property)) {
            $this->httpRequest->$property = $value;
        }

        return $this;
    }

    public function __isset($property)
    {
        return isset($this->httpRequest->$property);
    }

    public function __unset($property)
    {
        unset($this->httpRequest->$property);
    }

    public function __call($name, $arguments)
    {
        if (\method_exists($this->httpRequest, $name)) {
            return $this->httpRequest->$name(...$arguments);
        }

        $trace = debug_backtrace();
        $fileName = $trace[0]['file'] ?? __FILE__;
        $line = $trace[0]['line'] ?? __LINE__;
        trigger_error(\sprintf(
            'Attempted to call an undefined method named "%s" of class "%s" in %s on line %d',
            $name,
            \get_class($this),
            $fileName,
            $line
        ));

        return null;
    }
}

This trait will be used in our validation class. It implements core magic methods __get() and __set() to access properties from original request. As __set() should have pair method __isset we implemented it also there. To access native methods from original request class (such as getClientIp(), getPathInfo() etc.) we implemented __call() method.

Handle exception

Also we need a separate Exception class, which we will throw out in case the data is not valid. To access all error messages we implemented getResponseData() method. Let's have a look on Exception we will throw for the invalid request:

Code example

<?php

namespace App\Exception;

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Class ValidationException
 */
class ValidationException extends \RuntimeException
{
    /** @var ValidatorInterface */
    private $validator;

    /** @var ConstraintViolationList|null */
    private $violations;

    /** @var \Symfony\Component\PropertyAccess\PropertyAccessor */
    private $propertyAccessor;

    public function __construct(ValidatorInterface $validator, ConstraintViolationList $violations = null)
    {
        $message = 'The given data failed to pass validation.';

        parent::__construct($message);

        $this->validator = $validator;
        $this->violations = $violations;
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
    }

    public function getValidator() : ValidatorInterface
    {
        return $this->validator;
    }

    public function getResponseData() : array
    {
        $errors = [];

        if ($this->violations instanceof ConstraintViolationList) {
            $iterator = $this->violations->getIterator();

            /** @var ConstraintViolation $violation */
            foreach ($iterator as $key => $violation) {
                $entryErrors = (array) $this->propertyAccessor->getValue($errors, $violation->getPropertyPath());
                $entryErrors[] = $violation->getMessage();

                $this->propertyAccessor->setValue($errors, $violation->getPropertyPath(), $entryErrors);
            }
        }

        return $errors;
    }
}

ValidationException receive validator instance $validator and validation violations $violations as parameters. As Symfony validator implements \IteratorAggregate interface for violation list, in constructor we define $propertyAccessor, which gives us an oppportunity to access properties from $violations iterator.

While handling ValidationException in kernel of our application, we can access violation list via $exception->getResponseData().

Validation class

And now we can proceed with writing our validation class to auto-validate incoming requests:

Code example

<?php

namespace App\Requests;

use App\Exception\ValidationException;
use App\Traits\ValidationAwareTrait;
use Symfony\Component\HttpFoundation\FileBag;
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\ServerBag;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Class BaseValidation
 *
 * @property ParameterBag   $attributes
 * @property ParameterBag   $request
 * @property ParameterBag   $query
 * @property ServerBag      $server
 * @property FileBag        $files
 * @property ParameterBag   $cookies
 * @property HeaderBag      $headers
 *
 * @method   duplicate(array $query, array $request, array $attributes, array $cookies, array $files, array $server)
 * @method   overrideGlobals()
 */
abstract class BaseValidation
{
    use ValidationAwareTrait;

    /** @var ValidatorInterface */
    private $validator;

    final public function __construct(RequestStack $request)
    {
        $this->httpRequest = $request->getCurrentRequest();
        $this->validator = Validation::createValidator();

        $this->initialize();
    }

    final public function initialize() : void
    {
        if (!$this->passesAuthorization()) {
            $this->failedAuthorization();
        }

        $this->validate();
    }

    /**
     * Get all request parameters
     *
     * @return array
     */
    final public function all() : array
    {
        return $this->httpRequest->attributes->all()
            + $this->httpRequest->query->all()
            + $this->httpRequest->request->all()
            + $this->httpRequest->files->all();
    }

    /**
     * Returns list of constraints for validation
     *
     * @return Collection
     */
    abstract public function rules() : Collection;

    /**
     * Determine if the request passes the authorization check.
     *
     * @return bool
     */
    final protected function passesAuthorization() : bool
    {
        if (method_exists($this, 'authorize')) {
            return $this->authorize();
        }

        return true;
    }

    /**
     * Handle a failed authorization attempt.
     *
     * @return void
     * @throws AccessDeniedHttpException
     */
    final protected function failedAuthorization() : void
    {
        throw new AccessDeniedHttpException();
    }

    /**
     * @return bool
     * @throws ValidationException
     */
    final protected function validate() : bool
    {
        /** @var ConstraintViolationList $violations */
        $violations = $this->validator->validate($this->all(), $this->rules());

        if ($violations->count()) {
            throw new ValidationException($this->validator, $violations);
        }

        return true;
    }
}

When instance of BaseValidation will be created, auto-validation will be fired. Constructor get current request and create ValidatorInterface instance. Then it tries to validate data from request.

Since our application can request not only the verification of incoming data, but also the verification of the permissions to use this data, we will add an optional authorize() method in case we want to verify the user's permissions to send this request. This method should return boolean value. If this method returns false, AccessDeniedHttpException will be thrown.

NOTE: This implementation is incorrect according SRP, so you can split permissions check and validation of the data. To simplify the example, we show the implementation of both checks simultaneously.

If authorization passed successfully, we start validate request data according to validation rules, which we get from rules() method. This method should return Symfony\Component\Validator\Constraints\Collection to be used with Symfony validator. If any violations will occur, ValidationException will be thrown. If not, then we will have access to valid data in Controller (or any other place we initialize instance of BaseValidation).

Let's write request class with auto-validation

Well done! Now we have all we need to write custom request class. Let's write some request validation class to test how well it works.

Code example

<?php

namespace App\Requests;

use Symfony\Component\Validator\Constraints\{
    Collection, File, Image, Required
};
use Symfony\Component\Validator\Exception\{
    ConstraintDefinitionException,
    InvalidOptionsException,
    MissingOptionsException
};

class ImageUploadRequest extends BaseValidation
{
    private const ALLOW_EXTRA_FIELDS = true;
    private const ALLOW_MISSING_FIELDS = false;
    private const EXTRA_FIELDS_MESSAGE = 'This field was not expected.';
    private const MISSING_FIELDS_MESSAGE = 'This field is missing.';

    /**
     * @return Collection
     * @throws MissingOptionsException
     * @throws InvalidOptionsException
     * @throws ConstraintDefinitionException
     */
    public function rules() : Collection
    {
        return new Collection([
            'fields'                => $this->getFields(),
            'allowExtraFields'      => self::ALLOW_EXTRA_FIELDS,
            'allowMissingFields'    => self::ALLOW_MISSING_FIELDS,
            'extraFieldsMessage'    => self::EXTRA_FIELDS_MESSAGE,
            'missingFieldsMessage'  => self::MISSING_FIELDS_MESSAGE,
        ]);
    }

    /**
     * @return array
     * @throws MissingOptionsException
     * @throws InvalidOptionsException
     * @throws ConstraintDefinitionException
     */
    private function getFields() : array
    {
        return [
            'image'   => new Required([
                new File(),
                new Image([
                    'detectCorrupted'   => true,
                ]),
            ]),
        ];
    }
}

This example class uses to validate request, that contains an image to be uploaded to server. As you can see above, Symfony\Component\Validator\Constraints\Collection except field rules also accept setting how to handle this fields.

Use custom request

And now just see how easy and simple it is to use the data from the request in the Controller and be sure that we are working with the valid data. Now we do not need to do checks in the Controller.

Code example

<?php

namespace App\Controller\ImageUpload;

use App\Requests\ImageUploadRequest;
use App\Service\ImageUpload\UserImageManager;
use Liip\ImagineBundle\Model\FileBinary;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;

class UserImageController extends Controller
{
    /**
     * Upload user image
     *
     * @param UserFileManager     $fileManager
     * @param UserImageManager    $uploadManager
     * @param ImageUploadRequest  $request
     * @param int                 $userId
     *
     * @return JsonResponse
     */
    public function __invoke(
        UserFileManager $fileManager,
        UserImageManager $uploadManager,
        ImageUploadRequest $request,
        int $userId
    ) : JsonResponse {
        $filename = $fileManager->getFileName($userId);

        /** @var UploadedFile $image */
        $image = $request->files->get('image');
        $uploadManager->save(
            new FileBinary($image->getRealPath(), $image->getMimeType(), $image->guessExtension()),
            $filename
        );

        return $this->json([
            'message'   => sprintf('User image %s successfully uploaded', $filename),
            'fileName'  => $filename,
        ]);
    }
}