diff --git a/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php b/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php index caed1d1d59322..740313c76e22a 100644 --- a/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php +++ b/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php @@ -34,9 +34,11 @@ public function validate($value, Constraint $constraint) $messages = [$constraint->message]; foreach ($constraint->constraints as $key => $item) { - $violations = $validator->validate($value, $item, $this->context->getGroup()); + $executionContext = clone $this->context; + $executionContext->setNode($value, $this->context->getObject(), $this->context->getMetadata(), $this->context->getPropertyPath()); + $violations = $validator->inContext($executionContext)->validate($value, $item, $this->context->getGroup())->getViolations(); - if (0 === \count($violations)) { + if (\count($this->context->getViolations()) === \count($violations)) { return; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 4560793eb5da8..bffe99709182b 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -364,4 +364,9 @@ public function generateCacheKey($object) return $this->cachedObjectsRefs[$object]; } + + public function __clone() + { + $this->violations = clone $this->violations; + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php index f05d05e047017..b4fbab5575bab 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php @@ -18,15 +18,22 @@ use Symfony\Component\Validator\Constraints\Country; use Symfony\Component\Validator\Constraints\DivisibleBy; use Symfony\Component\Validator\Constraints\EqualTo; +use Symfony\Component\Validator\Constraints\Expression; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\IdenticalTo; use Symfony\Component\Validator\Constraints\Language; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\LessThan; use Symfony\Component\Validator\Constraints\Negative; +use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\Unique; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; use Symfony\Component\Validator\Validation; @@ -45,15 +52,7 @@ protected function createValidator() */ public function testValidCombinations($value, $constraints) { - $i = 0; - - foreach ($constraints as $constraint) { - $this->expectViolationsAt($i++, $value, $constraint); - } - - $this->validator->validate($value, new AtLeastOneOf($constraints)); - - $this->assertNoViolation(); + $this->assertCount(0, Validation::createValidator()->validate($value, new AtLeastOneOf($constraints))); } public function getValidCombinations() @@ -96,18 +95,20 @@ public function getValidCombinations() public function testInvalidCombinationsWithDefaultMessage($value, $constraints) { $atLeastOneOf = new AtLeastOneOf(['constraints' => $constraints]); + $validator = Validation::createValidator(); $message = [$atLeastOneOf->message]; $i = 0; foreach ($constraints as $constraint) { - $message[] = ' ['.($i + 1).'] '.$this->expectViolationsAt($i++, $value, $constraint)->get(0)->getMessage(); + $message[] = sprintf(' [%d] %s', ++$i, $validator->validate($value, $constraint)->get(0)->getMessage()); } - $this->validator->validate($value, $atLeastOneOf); + $violations = $validator->validate($value, $atLeastOneOf); - $this->buildViolation(implode('', $message))->setCode(AtLeastOneOf::AT_LEAST_ONE_OF_ERROR)->assertRaised(); + $this->assertCount(1, $violations, sprintf('1 violation expected. Got %u.', \count($violations))); + $this->assertEquals(new ConstraintViolation(implode('', $message), implode('', $message), [], $value, '', $value, null, AtLeastOneOf::AT_LEAST_ONE_OF_ERROR, $atLeastOneOf), $violations->get(0)); } /** @@ -117,15 +118,10 @@ public function testInvalidCombinationsWithCustomMessage($value, $constraints) { $atLeastOneOf = new AtLeastOneOf(['constraints' => $constraints, 'message' => 'foo', 'includeInternalMessages' => false]); - $i = 0; - - foreach ($constraints as $constraint) { - $this->expectViolationsAt($i++, $value, $constraint); - } + $violations = Validation::createValidator()->validate($value, $atLeastOneOf); - $this->validator->validate($value, $atLeastOneOf); - - $this->buildViolation('foo')->setCode(AtLeastOneOf::AT_LEAST_ONE_OF_ERROR)->assertRaised(); + $this->assertCount(1, $violations, sprintf('1 violation expected. Got %u.', \count($violations))); + $this->assertEquals(new ConstraintViolation('foo', 'foo', [], $value, '', $value, null, AtLeastOneOf::AT_LEAST_ONE_OF_ERROR, $atLeastOneOf), $violations->get(0)); } public function getInvalidCombinations() @@ -184,4 +180,40 @@ public function testGroupsArePropagatedToNestedConstraints() $this->assertCount(1, $violations); } + + public function testContextIsPropagatedToNestedConstraints() + { + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory(new class() implements MetadataFactoryInterface { + public function getMetadataFor($classOrObject): MetadataInterface + { + return (new ClassMetadata(ExpressionConstraintNested::class)) + ->addPropertyConstraint('foo', new AtLeastOneOf([ + new NotNull(), + new Expression('this.getFoobar() in ["bar", "baz"]'), + ])); + } + + public function hasMetadataFor($classOrObject): bool + { + return ExpressionConstraintNested::class === $classOrObject; + } + }) + ->getValidator() + ; + + $violations = $validator->validate(new ExpressionConstraintNested(), new Valid()); + + $this->assertCount(0, $violations); + } +} + +class ExpressionConstraintNested +{ + private $foo; + + public function getFoobar(): string + { + return 'bar'; + } }