Skip to content

Instantly share code, notes, and snippets.

@rybakit
Last active February 1, 2023 14:35
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save rybakit/4705749 to your computer and use it in GitHub Desktop.
Save rybakit/4705749 to your computer and use it in GitHub Desktop.
Nested chain validator for symfony > 2.1
<?php
namespace Acme\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/**
* @Annotation
*/
class Chain extends Constraint
{
public $constraints;
public $stopOnError = true;
/**
* {@inheritdoc}
*/
public function __construct($options = null)
{
// no known options set? $options is the constraints array
if (is_array($options) && !array_intersect(array_keys($options), array('groups', 'constraints', 'stopOnError'))) {
$options = array('constraints' => $options);
}
parent::__construct($options);
if (!is_array($this->constraints)) {
throw new ConstraintDefinitionException('The option "constraints" is expected to be an array in constraint '.__CLASS__.'.');
}
foreach ($this->constraints as $constraint) {
if (!$constraint instanceof Constraint) {
throw new ConstraintDefinitionException('The value '.$constraint.' is not an instance of Constraint in constraint '.__CLASS__.'.');
}
if ($constraint instanceof Valid) {
throw new ConstraintDefinitionException('The constraint Valid cannot be nested inside constraint '.__CLASS__.'.');
}
}
}
public function getRequiredOptions()
{
return array('constraints');
}
}
<?php
namespace Acme\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ChainValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$group = $this->context->getGroup();
$propertyPath = $this->context->getPropertyPath();
$violationList = $this->context->getViolations();
$violationCountPrevious = $violationList->count();
foreach ($constraint->constraints as $constr) {
$this->context->validateValue($value, $constr, $propertyPath, $group);
if ($constraint->stopOnError && (count($violationList) !== $violationCountPrevious)) {
return;
}
}
}
}
<?php
namespace Acme\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
class NoCacheConstraintValidatorFactory implements ConstraintValidatorFactoryInterface
{
/**
* @var ConstraintValidatorFactoryInterface
*/
private $factory;
/**
* @var array
*/
private $nonCachableClasses;
public function __construct(ConstraintValidatorFactoryInterface $factory, array $nonCachableClasses)
{
$this->factory = $factory;
$this->nonCachableClasses = $nonCachableClasses;
}
/**
* {@inheritDoc}
*/
public function getInstance(Constraint $constraint)
{
$className = $constraint->validatedBy();
if (in_array($className, $this->nonCachableClasses, true)) {
return new $className();
}
return $this->factory->getInstance($constraint);
}
}
<?php
use Symfony\Component\Validator\Constraints\Date;
use Symfony\Component\Validator\Constraints\Type;
use Acme\Validator\Constraints\Chain;
$constraint1 = new Chain([new Type('string'), new Date()]);
$constraint2 = new Chain([
'constraints' => [new Type('string'), new Date()],
'stopOnError' => true,
]);
// Symfony Validator Component v2.x has an issue with nested constraints,
// see https://github.com/symfony/Validator/blob/fc0650c1825c842f9dcc4819a2eaff9922a07e7c/ConstraintValidatorFactory.php#L48.
// If you plan to use nested `Chain` constraints, consider using the `NoCacheConstraintValidatorFactory` decorator.
// Here is a usage example for the Silex application:
$app->register(new Silex\Provider\ValidatorServiceProvider());
$app['validator.validator_factory'] = $app->share($app->extend('validator.validator_factory', function ($factory, $app) {
return new Acme\Validator\NoCacheConstraintValidatorFactory($factory, [
'Acme\\Validator\\Constraints\\ChainValidator',
]);
}));
@GromNaN
Copy link

GromNaN commented Apr 11, 2014

@sbarbosa115
Copy link

Very useful, thanks.

@nicholasnet
Copy link

As per your comment

If you plan to use nested Chain constraints, consider using the NoCacheConstraintValidatorFactory decorator.

Is this still valid for Symfony 3.3. Also how can this be used in Symfony project rather than Silex.

@appeltaert
Copy link

appeltaert commented Jan 29, 2018

Pretty good solution to prevent abusing validation groups. Only change required to get this working in >3.0 is:

            $this->context->getValidator()
                ->inContext($this->context)
                ->atPath($propertyPath)
                ->validate($value, $constr, $group);

instead of:

            $this->context->validateValue($value, $constr, $propertyPath, $group);

Dont need to use the NoCache factory either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment