Acyra
Acyra

Reputation: 16034

Using Custom Validators in Symfony Controllers

I want to use custom validators on various query parameters in my controller. The docs give this example:

 // validate a query parameter (a hash in this case)
    $incomingHashConstraint = new CustomAssert\IncomingHash();

    // use the validator to validate the value
    // If you're using the new 2.5 validation API (you probably are!)
    $errorList = $this->get('validator')->validate(
        $incomingHash,
        $incomingHashConstraint
    );

    if (0 === count($errorList)) {
        // ... this IS a valid hash
    } else {
        // this is *not* a valid hash
        $errorMessage = $errorList[0]->getMessage();

        // ... do something with the error
        throw $this->createNotFoundException('Not a valid hash ID ' . $incomingHash);
    }

It is pretty clunky to use this in a lot of controllers. Ideally I'd be able to use a custom validator as a requirement in the route, but that doesn't seem to be an option. Should these validators be a service? Ideally I'd want something like

if(!isValid($incomingHash, IncomingHashConstraint)) {
   throw \Exception(); }

Any suggestions on the best way to organize this? Thanks!

Upvotes: 1

Views: 2397

Answers (1)

BentCoder
BentCoder

Reputation: 12720

There is a very easy and clean way of doing it.

  1. The Request payload gets mapped to your custom model class and validated against you custom validation class.
  2. If there is an error then you get the list of errors.
  3. If there is no error then you get your model class back with all the payload data are mapped to the relevant properties in it then you use that model class for further logic(you should do extra logic in a service class not in controller).

I'll give a working example but if you want a full example then it is here. If you apply it, you'll have a very very thin controller. Literally no more than 15 lines. This way you'll have what exactly what @SergioIvanuzzo said above.

INSTALL jms/serializer-bundle Doc

composer require jms/serializer-bundle

// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new JMS\SerializerBundle\JMSSerializerBundle(),
    // ...
);

CUSTOM MODEL CLASS

namespace Application\FrontendBundle\Model;

use Application\FrontendBundle\Validator\Constraint as PersonAssert;
use JMS\Serializer\Annotation as Serializer;

/**
 * @PersonAssert\Person
 */
class Person
{
    /**
     * @var int
     * @Serializer\Type("integer")
     */
    public $id;

    /**
     * @var string
     * @Serializer\Type("string")
     */
    public $name;

    /**
     * @var string
     * @Serializer\Type("string")
     */
    public $dob;

    /**
     * @var string
     * @Serializer\Type("string")
     */
    public $whatever;
}

YOUR CUSTOM VALIDATOR CLASSES

namespace Application\FrontendBundle\Validator\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class Person extends Constraint
{
    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }

    public function validatedBy()
    {
        return get_class($this).'Validator';
    }
}

namespace Application\FrontendBundle\Validator\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class PersonValidator extends ConstraintValidator
{
    public function validate($person, Constraint $constraint)
    {
        if (!is_numeric($person->id)) {
            $this->context->buildViolation('Id must be a numeric value.')->addViolation();
        }

        if ($person->name == 'Acyra') {
            $this->context->buildViolation('You name is weird.')->addViolation();
        }

        if ($person->dob == '28/11/2014') {
            $this->context->buildViolation('You are too young.')->addViolation();
        }

        // I'm not interested in validating $whatever property of Person model!
    }
}

CONTROLLER

If you don't use your controller as a service then you can access validator and serializer services directly with $this->get('put_the_name_here') like you did above.

...
use JMS\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
....

/**
 * @Route("person", service="application_frontend.controller.bank")
 */
class PersonController extends Controller
{
    private $validator;
    private $serializer;

    public function __construct(
        ValidatorInterface $validator,
        SerializerInterface $serializer
    ) {
        $this->validator = $validator;
        $this->serializer = $serializer;
    }

    /**
     * @param Request $request
     *
     * @Route("/person")
     * @Method({"POST"})
     *
     * @return Response
     */
    public function personAction(Request $request)
    {
        $person = $this->validatePayload(
            $request->getContent(),
            'Application\FrontendBundle\Model\Person'
        );
        if ($person instanceof Response) {
            return $person;
        }

        print_r($person);
        // Now you can carry on doing things in your service class
    }

    private function validatePayload($payload, $model, $format = 'json')
    {
        $payload = $this->serializer->deserialize($payload, $model, $format);

        $errors = $this->validator->validate($payload);
        if (count($errors)) {
            return new Response('Some errors', 400);
        }

        return $payload;
    }
}

EXAMPLES

Request 1

{
  "id": 66,
  "name": "Acyraaaaa",
  "dob": "11/11/1111",
  "whatever": "test"
}

Response 1

Application\FrontendBundle\Model\Person Object
(
    [id] => 66
    [name] => Acyraaaaa
    [dob] => 11/11/1111
    [whatever] => test
)

Request 2

{
  "id": "Hello",
  "name": "Acyra",
  "dob": "28/11/2014"
}

Response 2

400 Bad request
Some errors

If you go to link I gave you above and apply the rest then you would actually get proper error messages like:

{
    "errors": {
        "id": "Id must be a numeric value.",
        "name": "You name is weird.",
        "dob": "You are too young."
    }
}

Upvotes: 4

Related Questions