diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 5d74eba99afb0..ef4ced3890932 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Deprecate the "loose" e-mail validation mode, use "html5" instead * Add the `negate` option to the `Expression` constraint, to inverse the logic of the violation's creation + * Add `is_valid` function to the `Expression` constraint, its behavior is the same as `ValidatorInterface::validate` 6.1 --- diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionLanguageProvider.php b/src/Symfony/Component/Validator/Constraints/ExpressionLanguageProvider.php new file mode 100644 index 0000000000000..9048e299f8fce --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/ExpressionLanguageProvider.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + +/** + * @author Ihor Khokhlov + */ +class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface +{ + 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(); + }), + ]; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php index 0e8ecac6b0093..80a924e9debe6 100644 --- a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php @@ -27,6 +27,7 @@ class ExpressionValidator extends ConstraintValidator public function __construct(ExpressionLanguage $expressionLanguage = null) { $this->expressionLanguage = $expressionLanguage; + $this->expressionLanguage?->registerProvider(new ExpressionLanguageProvider()); } /** @@ -41,6 +42,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) @@ -54,6 +56,7 @@ private function getExpressionLanguage(): ExpressionLanguage { if (null === $this->expressionLanguage) { $this->expressionLanguage = new ExpressionLanguage(); + $this->expressionLanguage->registerProvider(new ExpressionLanguageProvider()); } return $this->expressionLanguage; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageProviderTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageProviderTest.php new file mode 100644 index 0000000000000..add2051e97942 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionLanguageProviderTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Constraints; + +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; +use Symfony\Component\Validator\Constraints\ExpressionValidator; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +class ExpressionLanguageProviderTest extends ConstraintValidatorTestCase +{ + protected function createValidator() + { + return new ExpressionValidator(); + } + + /** + * @dataProvider dataProviderCompile + */ + public function testCompile(string $expression, array $names, string $expected) + { + $provider = new ExpressionLanguageProvider(); + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->registerProvider($provider); + + $result = $expressionLanguage->compile($expression, $names); + + $this->assertSame($expected, $result); + } + + public function dataProviderCompile(): 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()', + ], + ]; + } + + public function testEvaluateValid() + { + $constraints = [new Length(['min' => 2, 'max' => 12])]; + + $this->expectValidateValue(0, 'foo', $constraints); + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->registerProvider(new ExpressionLanguageProvider()); + + $this->assertTrue($expressionLanguage->evaluate('is_valid("foo", a)', ['a' => $constraints, 'context' => $this->context])); + } + + public function testEvaluateInvalid() + { + $constraints = [new Length(['min' => 7, 'max' => 12])]; + + $this->expectFailingValueValidation( + 0, + 'foo', + $constraints, + null, + new ConstraintViolation('error_length', '', [], '', '', 'foo', null, 'range') + ); + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->registerProvider(new ExpressionLanguageProvider()); + + $this->assertFalse($expressionLanguage->evaluate('is_valid("foo", a)', ['a' => $constraints, 'context' => $this->context])); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php index 9447648f35a9e..6df36c05315b6 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\Annotation\Entity; use Symfony\Component\Validator\Tests\Fixtures\ToString; @@ -304,4 +307,50 @@ 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()); + } }