From 2cbde2e0fd1535545cb58c41fdf8432742ff9621 Mon Sep 17 00:00:00 2001 From: Andrey Sevastianov Date: Fri, 21 Feb 2020 13:17:04 +0200 Subject: [PATCH] [ExpressionLanguage] Added expression language syntax validator --- CHANGELOG.md | 1 + Constraints/ExpressionLanguageSyntax.php | 42 +++++++++ .../ExpressionLanguageSyntaxValidator.php | 55 ++++++++++++ .../ExpressionLanguageSyntaxTest.php | 88 +++++++++++++++++++ composer.json | 5 +- 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 Constraints/ExpressionLanguageSyntax.php create mode 100644 Constraints/ExpressionLanguageSyntaxValidator.php create mode 100644 Tests/Constraints/ExpressionLanguageSyntaxTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d2eb00fc4..9921ef6d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * allow to define a reusable set of constraints by extending the `Compound` constraint * added `Sequentially` constraint, to sequentially validate a set of constraints (any violation raised will prevent further validation of the nested constraints) * added the `divisibleBy` option to the `Count` constraint + * added the `ExpressionLanguageSyntax` constraint 5.0.0 ----- diff --git a/Constraints/ExpressionLanguageSyntax.php b/Constraints/ExpressionLanguageSyntax.php new file mode 100644 index 000000000..7391554ac --- /dev/null +++ b/Constraints/ExpressionLanguageSyntax.php @@ -0,0 +1,42 @@ + + * + * 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\Validator\Constraint; + +/** + * @Annotation + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * + * @author Andrey Sevastianov + */ +class ExpressionLanguageSyntax extends Constraint +{ + const EXPRESSION_LANGUAGE_SYNTAX_ERROR = '1766a3f3-ff03-40eb-b053-ab7aa23d988a'; + + protected static $errorNames = [ + self::EXPRESSION_LANGUAGE_SYNTAX_ERROR => 'EXPRESSION_LANGUAGE_SYNTAX_ERROR', + ]; + + public $message = 'This value should be a valid expression.'; + public $service; + public $validateNames = true; + public $names = []; + + /** + * {@inheritdoc} + */ + public function validatedBy() + { + return $this->service; + } +} diff --git a/Constraints/ExpressionLanguageSyntaxValidator.php b/Constraints/ExpressionLanguageSyntaxValidator.php new file mode 100644 index 000000000..4a02d49a8 --- /dev/null +++ b/Constraints/ExpressionLanguageSyntaxValidator.php @@ -0,0 +1,55 @@ + + * + * 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\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\SyntaxError; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +/** + * @author Andrey Sevastianov + */ +class ExpressionLanguageSyntaxValidator extends ConstraintValidator +{ + private $expressionLanguage; + + public function __construct(ExpressionLanguage $expressionLanguage) + { + $this->expressionLanguage = $expressionLanguage; + } + + /** + * {@inheritdoc} + */ + public function validate($expression, Constraint $constraint): void + { + if (!$constraint instanceof ExpressionLanguageSyntax) { + throw new UnexpectedTypeException($constraint, ExpressionLanguageSyntax::class); + } + + if (!\is_string($expression)) { + throw new UnexpectedTypeException($expression, 'string'); + } + + try { + $this->expressionLanguage->lint($expression, ($constraint->validateNames ? ($constraint->names ?? []) : null)); + } catch (SyntaxError $exception) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ syntax_error }}', $this->formatValue($exception->getMessage())) + ->setInvalidValue((string) $expression) + ->setCode(ExpressionLanguageSyntax::EXPRESSION_LANGUAGE_SYNTAX_ERROR) + ->addViolation(); + } + } +} diff --git a/Tests/Constraints/ExpressionLanguageSyntaxTest.php b/Tests/Constraints/ExpressionLanguageSyntaxTest.php new file mode 100644 index 000000000..dc80288c3 --- /dev/null +++ b/Tests/Constraints/ExpressionLanguageSyntaxTest.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 Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\SyntaxError; +use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntax; +use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntaxValidator; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +class ExpressionLanguageSyntaxTest extends ConstraintValidatorTestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\MockObject|ExpressionLanguage + */ + protected $expressionLanguage; + + protected function createValidator() + { + return new ExpressionLanguageSyntaxValidator($this->expressionLanguage); + } + + protected function setUp(): void + { + $this->expressionLanguage = $this->createExpressionLanguage(); + + parent::setUp(); + } + + public function testExpressionValid(): void + { + $this->expressionLanguage->expects($this->once()) + ->method('lint') + ->with($this->value, []); + + $this->validator->validate($this->value, new ExpressionLanguageSyntax([ + 'message' => 'myMessage', + ])); + + $this->assertNoViolation(); + } + + public function testExpressionWithoutNames(): void + { + $this->expressionLanguage->expects($this->once()) + ->method('lint') + ->with($this->value, null); + + $this->validator->validate($this->value, new ExpressionLanguageSyntax([ + 'message' => 'myMessage', + 'validateNames' => false, + ])); + + $this->assertNoViolation(); + } + + public function testExpressionIsNotValid(): void + { + $this->expressionLanguage->expects($this->once()) + ->method('lint') + ->with($this->value, []) + ->willThrowException(new SyntaxError('Test exception', 42)); + + $this->validator->validate($this->value, new ExpressionLanguageSyntax([ + 'message' => 'myMessage', + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ syntax_error }}', '"Test exception around position 42."') + ->setCode(ExpressionLanguageSyntax::EXPRESSION_LANGUAGE_SYNTAX_ERROR) + ->assertRaised(); + } + + protected function createExpressionLanguage(): MockObject + { + return $this->getMockBuilder('\Symfony\Component\ExpressionLanguage\ExpressionLanguage')->getMock(); + } +} diff --git a/composer.json b/composer.json index a96e11fcc..0ed2945d3 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "symfony/yaml": "^4.4|^5.0", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", + "symfony/expression-language": "^5.1", "symfony/cache": "^4.4|^5.0", "symfony/mime": "^4.4|^5.0", "symfony/property-access": "^4.4|^5.0", @@ -44,6 +44,7 @@ "doctrine/lexer": "<1.0.2", "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<4.4", + "symfony/expression-language": "<5.1", "symfony/http-kernel": "<4.4", "symfony/intl": "<4.4", "symfony/translation": "<4.4", @@ -61,7 +62,7 @@ "egulias/email-validator": "Strict (RFC compliant) email validation", "symfony/property-access": "For accessing properties within comparison constraints", "symfony/property-info": "To automatically add NotNull and Type constraints", - "symfony/expression-language": "For using the Expression validator" + "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints" }, "autoload": { "psr-4": { "Symfony\\Component\\Validator\\": "" },