Symfony2: Creating a Validator with dependencies? Make it a service!

One of the ugly things about Symfony 1 validators was that their dependencies were generally fetched from far away, mainly by calling sfContext::getInstance(). With Symfony2 and it’s service container, this isn’t necessary anymore. Validators can be services. The example below is quite simple, but given you can inject anything you want, you can make it quite complicated. I show you how to create a validator that checks with the router of the value-under-validation is a route.

For validation two things are needed: a Constraint and a Validator. The Validator::isValid() method, receives two argument: the value to validate and a Constraint. The Constraint may have several options, and for example a message to use in case the value is not valid. These options and a message can be set as a public property.

First create the Route Constraint, e.g. in /src/Acme/DemoBundle/Validator/Constraints/Route.php

namespace Acme\DemoBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

class Route extends Constraint
{
    public $message = 'This route does not exist';

    public function validatedBy()
    {
        return 'acme.validator.route';
    }
}

The method validatedBy() returns a string, which is normally the class of the validator for this constraint (by default created by simply appending the string “Validator” to the class of “Constraint”). In this case, the method returns the id of the service that will be the validator.

Let’s create the validator first and later make it into a service.


namespace Acme\DemoBundle\Validator\Constraints;

use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Router;

class RouteValidator extends ConstraintValidator
{
    protected $router;

    public function __construct(Router $router)
    {
        $this->router = $router;
    }

    public function isValid($value, Constraint $constraint)
    {
        if (null === $value || '' === $value) {
            return true;
        }

        if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
            throw new UnexpectedTypeException($value, 'string');
        }

        $value = (string) $value;

        try {
            $route = $this->router->getGenerator()->generate($value);
            $valid = true;
        }
        catch (RouteNotFoundException $e) {
            $valid = false;
        }
        catch (MissingMandatoryParametersException $e) {
            // the route exists, but generate() trips over a missing parameter
            $valid = true;
        }
        catch (InvalidParameterException $e) {
            // the route exists, but generate() trips over an invalid parameter
            $valid = true;
        }

        if (!$valid) {
            $this->setMessage($constraint->message, array('{{ value }}' => $value));

            return false;
        }

        return true;
    }
}

Finally, let’s make our new validator known to the service container and more specifically to the validator, by adding a tag to the service: “validator.constraint_validator”. So the validator knows that a new constraint validator is in town.

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="acme.validator.route" class="Acme\DemoBundle\Validator\Constraints\RouteValidator">
            <argument type="service" id="router" />
            <tag name="validator.constraint_validator" alias="acme.validator.route" />
        </service>
    </services>
</container>

Now, we can use the new Route constraint in the usual way. Or for example use it separately:

$validator = $this->get('acme.validator.route'); // get the validator from the service container

$constraint = new RouteConstraint();

if (!$validator->isValid('_welcome', $constraint)) {
    // the route exists
}

Posted in Service, Symfony2, Validation | 8 Comments

8 Responses to Symfony2: Creating a Validator with dependencies? Make it a service!

  • # December 4, 2011 at 15:51

    Nice, I was looking for documentation on this. Does this work with annotations as well or is something else needed for that?

    charlie

    Reply

  • # December 5, 2011 at 19:52

    I’m not sure, I haven’t used much of the annotation related features; by the way, this post is almost something of a cookbook article, so I might fork the documentation repository and a page about this.

    Matthias Noback

    Reply

  • # December 6, 2011 at 00:53

    To be able to use it as annotation, you simply need to add @Annotation in the phpdoc (like for core constraint) and then use the class as annotation:


    <?php

    use Acme\DemoBundle\Validator\Constraints\Route;

    // …
    /**
    * @Route(message="This should be a route")
    */
    protected $route;

    I'm not sure if the code example will be displayed properly in the comment.

    Stof

    Reply

    • # December 6, 2011 at 21:05

      Thanks for your suggestion!

      Matthias Noback

      Reply

  • # December 6, 2011 at 00:56

    And just a though about your validator implementation: $router->getRouteCollection() should probably be avoided as it is really bad for performances. It will load all routing files which can be expensive, whereas the prod environment is generally meant to do it only on the first request.

    Stof

    Reply

    • # December 6, 2011 at 21:04

      Hi, thanks for your comment. I looked the method up in Symfony\Component\Routing\Router and it appears getRouteCollection makes only one call to the loader, and then stores the result in it’s attribute “collection”, so I think this should not be a problem for performance (oh, actually, this takes place in Symfony\Bundle\FrameworkBundle\Routing\Router)

      Matthias Noback

      Reply

      • # December 6, 2011 at 21:19

        The point is, in prod, it uses the cached router so the collection is not loaded at all

        Stof

        Reply

        • # December 15, 2011 at 12:33

          Ah, I get it. I just updated the post so the validation makes use of the UrlGenerator. In prod as well as dev, this makes use of the cached router.

          Matthias Noback

          Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

* *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>