Skip to content

Instantly share code, notes, and snippets.

@rybakit
Last active February 1, 2023 14:35
Show Gist options
  • 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',
]);
}));
@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