Symfony2: Creating a Validator with dependencies? Make it a service!
Matthias Noback
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="https://symfony.com/schema/dic/services"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://symfony.com/schema/dic/services https://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
}