diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 183267cf26287..addca159de6a3 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 6.4 --- + * Add `is_valid` function to the `Expression` constraint, its behavior is the same as `ValidatorInterface::validate` * Allow single integer for the `versions` option of the `Uuid` constraint * Allow single constraint to be passed to the `constraints` option of the `When` constraint * Deprecate Doctrine annotations support in favor of native attributes diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php index d1fe60a791084..45026f878471c 100644 --- a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -20,13 +22,16 @@ * @author Fabien Potencier * @author Bernhard Schussek */ -class ExpressionValidator extends ConstraintValidator +class ExpressionValidator extends ConstraintValidator implements ExpressionFunctionProviderInterface { - private ?ExpressionLanguage $expressionLanguage; + private ExpressionLanguage $expressionLanguage; public function __construct(ExpressionLanguage $expressionLanguage = null) { - $this->expressionLanguage = $expressionLanguage; + if ($expressionLanguage) { + $this->expressionLanguage = clone $expressionLanguage; + $this->expressionLanguage->registerProvider($this); + } } /** @@ -41,6 +46,7 @@ public function validate(mixed $value, Constraint $constraint) $variables = $constraint->values; $variables['value'] = $value; $variables['this'] = $this->context->getObject(); + $variables['context'] = $this->context; if ($constraint->negate xor $this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) { $this->context->buildViolation($constraint->message) @@ -50,8 +56,27 @@ public function validate(mixed $value, Constraint $constraint) } } + public function getFunctions(): array + { + return [ + new ExpressionFunction('is_valid', function (...$arguments) { + return sprintf( + '0 === $context->getValidator()->inContext($context)->validate(%s)->getViolations()->count()', + implode(', ', $arguments) + ); + }, function (array $variables, ...$arguments): bool { + return 0 === $variables['context']->getValidator()->inContext($variables['context'])->validate(...$arguments)->getViolations()->count(); + }), + ]; + } + private function getExpressionLanguage(): ExpressionLanguage { - return $this->expressionLanguage ??= new ExpressionLanguage(); + if (!isset($this->expressionLanguage)) { + $this->expressionLanguage = new ExpressionLanguage(); + $this->expressionLanguage->registerProvider($this); + } + + return $this->expressionLanguage; } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php index f427604725a2f..132507c923af7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php @@ -14,6 +14,9 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Validator\Constraints\Expression; use Symfony\Component\Validator\Constraints\ExpressionValidator; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; use Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity; use Symfony\Component\Validator\Tests\Fixtures\ToString; @@ -304,4 +307,81 @@ public function testViolationOnPass() ->setCode(Expression::EXPRESSION_FAILED_ERROR) ->assertRaised(); } + + public function testIsValidExpression() + { + $constraints = [new NotNull(), new Range(['min' => 2])]; + + $constraint = new Expression( + ['expression' => 'is_valid(this.data, a)', 'values' => ['a' => $constraints]] + ); + + $object = new Entity(); + $object->data = 7; + + $this->setObject($object); + + $this->expectValidateValue(0, $object->data, $constraints); + + $this->validator->validate($object, $constraint); + + $this->assertNoViolation(); + } + + public function testIsValidExpressionInvalid() + { + $constraints = [new Range(['min' => 2, 'max' => 5])]; + + $constraint = new Expression( + ['expression' => 'is_valid(this.data, a)', 'values' => ['a' => $constraints]] + ); + + $object = new Entity(); + $object->data = 7; + + $this->setObject($object); + + $this->expectFailingValueValidation( + 0, + 7, + $constraints, + null, + new ConstraintViolation('error_range', '', [], '', '', 7, null, 'range') + ); + + $this->validator->validate($object, $constraint); + + $this->assertCount(2, $this->context->getViolations()); + } + + /** + * @dataProvider provideCompileIsValid + */ + public function testCompileIsValid(string $expression, array $names, string $expected) + { + $provider = new ExpressionValidator(); + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->registerProvider($provider); + + $result = $expressionLanguage->compile($expression, $names); + + $this->assertSame($expected, $result); + } + + public static function provideCompileIsValid(): array + { + return [ + [ + 'is_valid("foo", constraints)', + ['constraints'], + '0 === $context->getValidator()->inContext($context)->validate("foo", $constraints)->getViolations()->count()', + ], + [ + 'is_valid(this.data, constraints, groups)', + ['this', 'constraints', 'groups'], + '0 === $context->getValidator()->inContext($context)->validate($this->data, $constraints, $groups)->getViolations()->count()', + ], + ]; + } }