Skip to content

Commit a8f450d

Browse files
committed
Add the twig constraint and its validator
1 parent 95d1191 commit a8f450d

File tree

5 files changed

+242
-0
lines changed

5 files changed

+242
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add the `Week` constraint
1414
* Add `CompoundConstraintTestCase` to ease testing Compound Constraints
1515
* Add context variable to `WhenValidator`
16+
* Add the `twig` constraint for validating Twig content
1617

1718
7.1
1819
---
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Attribute\HasNamedArguments;
15+
use Symfony\Component\Validator\Constraint;
16+
use Symfony\Component\Validator\Exception\LogicException;
17+
use Twig\Environment;
18+
19+
/**
20+
* @author Mokhtar Tlili <tlili.mokhtar@gmail.com>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
23+
class Twig extends Constraint
24+
{
25+
public const INVALID_TWIG_ERROR = 'e7fc55d5-e586-4cc1-924e-d27ee7fcd1b5';
26+
27+
protected const ERROR_NAMES = [
28+
self::INVALID_TWIG_ERROR => 'INVALID_TWIG_ERROR',
29+
];
30+
31+
#[HasNamedArguments]
32+
public function __construct(
33+
public string $message = 'This value is not valid Twig.',
34+
?array $groups = null,
35+
mixed $payload = null,
36+
) {
37+
if (!class_exists(Environment::class)) {
38+
throw new LogicException('The twig/twig library is required to use the Twig constraint. Try running "composer require twig/twig".');
39+
}
40+
41+
parent::__construct(null, $groups, $payload);
42+
}
43+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
use Twig\Environment;
19+
use Twig\Error\Error;
20+
use Twig\Loader\ArrayLoader;
21+
use Twig\Source;
22+
23+
/**
24+
* @author Mokhtar Tlili <tlili.mokhtar@gmail.com>
25+
*/
26+
class TwigValidator extends ConstraintValidator
27+
{
28+
public function validate(mixed $value, Constraint $constraint): void
29+
{
30+
if (!$constraint instanceof Twig) {
31+
throw new UnexpectedTypeException($constraint, Twig::class);
32+
}
33+
34+
if (null === $value || '' === $value) {
35+
return;
36+
}
37+
38+
if (!\is_scalar($value) && !$value instanceof \Stringable) {
39+
throw new UnexpectedValueException($value, 'string');
40+
}
41+
42+
$value = (string) $value;
43+
44+
$prevErrorHandler = set_error_handler(static function ($level, $message, $file, $line) use (&$prevErrorHandler) {
45+
if (\E_USER_DEPRECATED === $level) {
46+
$templateLine = 0;
47+
if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) {
48+
$templateLine = $matches[1];
49+
}
50+
51+
throw new Error($message, $templateLine);
52+
}
53+
54+
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
55+
});
56+
57+
try {
58+
$twig = new Environment(new ArrayLoader([$value]));
59+
$twig->parse($twig->tokenize(new Source($value, '')));
60+
} catch (Error $e) {
61+
$this->context->buildViolation($constraint->message)
62+
->setParameter('{{ error }}', $e->getMessage())
63+
->setParameter('{{ line }}', $e->getTemplateLine())
64+
->setCode(Twig::INVALID_TWIG_ERROR)
65+
->addViolation();
66+
} finally {
67+
restore_error_handler();
68+
}
69+
}
70+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\Twig;
16+
use Symfony\Component\Validator\Mapping\ClassMetadata;
17+
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
18+
19+
/**
20+
* @author Mokhtar Tlili <tlili.mokhtar@gmail.com>
21+
*/
22+
class TwigTest extends TestCase
23+
{
24+
public function testAttributes()
25+
{
26+
$metadata = new ClassMetadata(TwigDummy::class);
27+
$loader = new AttributeLoader();
28+
self::assertTrue($loader->loadClassMetadata($metadata));
29+
30+
[$bConstraint] = $metadata->properties['b']->getConstraints();
31+
self::assertSame('myMessage', $bConstraint->message);
32+
self::assertSame(['Default', 'TwigDummy'], $bConstraint->groups);
33+
34+
[$cConstraint] = $metadata->properties['c']->getConstraints();
35+
self::assertSame(['my_group'], $cConstraint->groups);
36+
self::assertSame('some attached data', $cConstraint->payload);
37+
}
38+
}
39+
40+
class TwigDummy
41+
{
42+
#[Twig]
43+
private $a;
44+
45+
#[Twig(message: 'myMessage')]
46+
private $b;
47+
48+
#[Twig(groups: ['my_group'], payload: 'some attached data')]
49+
private $c;
50+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use Symfony\Component\Validator\Constraints\Twig;
15+
use Symfony\Component\Validator\Constraints\TwigValidator;
16+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
17+
18+
/**
19+
* @author Mokhtar Tlili <tlili.mokhtar@gmail.com>
20+
*/
21+
class TwigValidatorTest extends ConstraintValidatorTestCase
22+
{
23+
protected function createValidator(): TwigValidator
24+
{
25+
return new TwigValidator();
26+
}
27+
28+
/**
29+
* @dataProvider getValidValues
30+
*/
31+
public function testTwigIsValid($value)
32+
{
33+
$this->validator->validate($value, new Twig());
34+
35+
$this->assertNoViolation();
36+
}
37+
38+
/**
39+
* @dataProvider getInvalidValues
40+
*/
41+
public function testInvalidValues($value, $message, $line)
42+
{
43+
$constraint = new Twig('myMessageTest');
44+
45+
$this->validator->validate($value, $constraint);
46+
47+
$this->buildViolation('myMessageTest')
48+
->setParameter('{{ error }}', $message)
49+
->setParameter('{{ line }}', $line)
50+
->setCode(Twig::INVALID_TWIG_ERROR)
51+
->assertRaised();
52+
}
53+
54+
public static function getValidValues()
55+
{
56+
return [
57+
['Hello {{ name }}'],
58+
['{% if condition %}Yes{% else %}No{% endif %}'],
59+
['{# Comment #}'],
60+
['Hello {{ "world" | upper }}'],
61+
['{% for i in 1..3 %}Item {{ i }}{% endfor %}'],
62+
];
63+
}
64+
65+
public static function getInvalidValues()
66+
{
67+
return [
68+
// Invalid syntax example (missing end tag)
69+
['{% if condition %}Oops', 'Unexpected end of template at line 1.', 1],
70+
// Another syntax error example (unclosed variable)
71+
['Hello {{ name', 'Unexpected token "end of template" ("end of print statement" expected) at line 1.', 1],
72+
// Unknown filter error
73+
['Hello {{ name | unknown_filter }}', 'Unknown "unknown_filter" filter at line 1.', 1],
74+
// Invalid variable syntax
75+
['Hello {{ .name }}', 'Unexpected token "punctuation" of value "." at line 1.', 1],
76+
];
77+
}
78+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy