8000 Add the twig constraint and its validator · symfony/symfony@e16a997 · GitHub
[go: up one dir, main page]

Skip to content

Commit e16a997

Browse files
sfmokfabpot
authored andcommitted
Add the twig constraint and its validator
1 parent 23e2230 commit e16a997

File tree

9 files changed

+338
-0
lines changed

9 files changed

+338
-0
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ CHANGELOG
44
7.3
55
---
66

7+
<<<<<<< HEAD
78
* Add `is_granted_for_user()` Twig function
89
* Add `field_id()` Twig form helper function
10+
=======
11+
* Add the `Twig` constraint for validating Twig templates
12+
>>>>>>> dd76f094f4a (Add the twig constraint and its validator)
913
1014
7.2
1115
---
1216

1317
* Deprecate passing a tag to the constructor of `FormThemeNode`
18+
* Add the `Twig` constraint for validating Twig content
1419

1520
7.1
1621
---
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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\Bridge\Twig\Tests\Validator\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\Twig\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+
[$dConstraint] = $metadata->properties['d']->getConstraints();
39+
self::assertFalse($dConstraint->skipDeprecations);
40+
}
41+
}
42+
43+
class TwigDummy
44+
{
45+
#[Twig]
46+
private $a;
47+
48+
#[Twig(message: 'myMessage')]
49+
private $b;
50+
51+
#[Twig(groups: ['my_group'], payload: 'some attached data')]
52+
private $c;
53+
54+
#[Twig(skipDeprecations: false)]
55+
private $d;
56+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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\Bridge\Twig\Tests\Validator\Constraints;
13+
14+
use Symfony\Bridge\Twig\Validator\Constraints\Twig;
15+
use Symfony\Bridge\Twig\Validator\Constraints\TwigValidator;
16+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
17+
use Twig\DeprecatedCallableInfo;
18+
use Twig\Environment;
19+
use Twig\Loader\ArrayLoader;
20+
use Twig\TwigFilter;
21+
22+
/**
23+
* @author Mokhtar Tlili <tlili.mokhtar@gmail.com>
24+
*/
25+
class TwigValidatorTest extends ConstraintValidatorTestCase
26+
{
27+
protected function createValidator(): TwigValidator
28+
{
29+
$environment = new Environment(new ArrayLoader());
30+
$environment->addFilter(new TwigFilter('humanize_filter', fn ($v) => $v));
31+
if (class_exists(DeprecatedCallableInfo::class)) {
32+
$options = ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1')];
33+
} else {
34+
$options = ['deprecated' => true];
35+
}
36+
37+
$environment->addFilter(new TwigFilter('deprecated_filter', fn ($v) => $v, $options));
38+
39+
return new TwigValidator($environment);
40+
}
41+
42+
/**
43+
* @dataProvider getValidValues
44+
*/
45+
public function testTwigIsValid($value)
46+
{
47+
$this->validator->validate($value, new Twig());
48+
49+
$this->assertNoViolation();
50+
}
51+
52+
/**
53+
* @dataProvider getInvalidValues
54+
*/
55+
public function testInvalidValues($value, $message, $line)
56+
{
57+
$constraint = new Twig('myMessageTest');
58+
59+
$this->validator->validate($value, $constraint);
60+
61+
$this->buildViolation('myMessageTest')
62+
->setParameter('{{ error }}', $message)
63+
->setParameter('{{ line }}', $line)
64+
->setCode(Twig::INVALID_TWIG_ERROR)
65+
->assertRaised();
66+
}
67+
68+
/**
69+
* When deprecations are skipped by the validator, the testsuite reporter will catch them so we need to mark the test as legacy.
70+
*
71+
* @group legacy
72+
*/
73+
public function testTwigWithSkipDeprecation()
74+
{
75+
$constraint = new Twig(skipDeprecations: true);
76+
77+
$this->validator->validate('{{ name|deprecated_filter }}', $constraint);
78+
79+
$this->assertNoViolation();
80+
}
81+
82+
public function testTwigWithoutSkipDeprecation()
83+
{
84+
$constraint = new Twig(skipDeprecations: false);
85+
86+
$this->validator->validate('{{ name|deprecated_filter }}', $constraint);
87+
88+
$line = 1;
89+
$error = 'Twig Filter "deprecated_filter" is deprecated in at line 1 at line 1.';
90+
if (class_exists(DeprecatedCallableInfo::class)) {
91+
$line = 0;
92+
$error = 'Since foo/bar 1.1: Twig Filter "deprecated_filter" is deprecated.';
93+
}
94+
$this->buildViolation($constraint->message)
95+
->setParameter('{{ error }}', $error)
96+
->setParameter('{{ line }}', $line)
97+
->setCode(Twig::INVALID_TWIG_ERROR)
98+
->assertRaised();
99+
}
100+
101+
public static function getValidValues()
102+
{
103+
return [
104+
['Hello {{ name }}'],
105+
['{% if condition %}Yes{% else %}No{% endif %}'],
106+
['{# Comment #}'],
107+
['Hello {{ "world"|upper }}'],
108+
['{% for i in 1..3 %}Item {{ i }}{% endfor %}'],
109+
['{{ name|humanize_filter }}'],
110+
];
111+
}
112+
113+
public static function getInvalidValues()
114+
{
115+
return [
116+
// Invalid syntax example (missing end tag)
117+
['{% if condition %}Oops', 'Unexpected end of template at line 1.', 1],
118+
// Another syntax error example (unclosed variable)
119+
['Hello {{ name', 'Unexpected token "end of template" ("end of print statement" expected) at line 1.', 1],
120+
// Unknown filter error
121+
['Hello {{ name|unknown_filter }}', 'Unknown "unknown_filter" filter at line 1.', 1],
122+
// Invalid variable syntax
123+
['Hello {{ .name }}', 'Unexpected token "punctuation" of value "." at line 1.', 1],
124+
];
125+
}
126+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\Bridge\Twig\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Attribute\HasNamedArguments;
15+
use Symfony\Component\Validator\Constraint;
16+
17+
/**
18+
* @author Mokhtar Tlili <tlili.mokhtar@gmail.com>
19+
*/
20+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
21+
class Twig extends Constraint
22+
{
23+
public const INVALID_TWIG_ERROR = 'e7fc55d5-e586-4cc1-924e-d27ee7fcd1b5';
24+
25+
protected const ERROR_NAMES = [
26+
self::INVALID_TWIG_ERROR => 'INVALID_TWIG_ERROR',
27+
];
28+
29+
#[HasNamedArguments]
30+
public function __construct(
31+
public string $message = 'This value is not a valid Twig template.',
32+
public bool $skipDeprecations = true,
33+
?array $groups = null,
34+
mixed $payload = null,
35+
) {
36+
parent::__construct(null, $groups, $payload);
37+
}
38+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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\Bridge\Twig\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 __construct(private Environment $twig)
29+
{
30+
}
31+
32+
public function validate(mixed $value, Constraint $constraint): void
33+
{
34+
if (!$constraint instanceof Twig) {
35+
throw new UnexpectedTypeException($constraint, Twig::class);
36+
}
37+
38+
if (null === $value || '' === $value) {
39+
return;
40+
}
41+
42+
if (!\is_scalar($value) && !$value instanceof \Stringable) {
43+
throw new UnexpectedValueException($value, 'string');
44+
}
45+
46+
$value = (string) $value;
47+
48+
if (!$constraint->skipDeprecations) {
49+
$prevErrorHandler = set_error_handler(static function ($level, $message, $file, $line) use (&$prevErrorHandler) {
50+
if (\E_USER_DEPRECATED !== $level) {
51+
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
52+
}
53+
54+
$templateLine = 0;
55+
if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) {
56+
$templateLine = $matches[1];
57+
}
58+
59+
throw new Error($message, $templateLine);
60+
});
61+
}
62+
63+
$realLoader = $this->twig->getLoader();
64+
try {
65+
$temporaryLoader = new ArrayLoader([$value]);
66+
$this->twig->setLoader($temporaryLoader);
67+
$this->twig->parse($this->twig->tokenize(new Source($value, '')));
68+
} catch (Error $e) {
69+
$this->context->buildViolation($constraint->message)
70+
->setParameter('{{ error }}', $e->getMessage())
71+
->setParameter('{{ line }}', $e->getTemplateLine())
72+
->setCode(Twig::INVALID_TWIG_ERROR)
73+
->addViolation();
74+
} finally {
75+
$this->twig->setLoader($realLoader);
76+
if (!$constraint->skipDeprecations) {
77+
restore_error_handler();
78+
}
79+
}
80+
}
81+
}

src/Symfony/Bridge/Twig/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"symfony/property-info": "^6.4|^7.0",
4141
"symfony/routing": "^6.4|^7.0",
4242
"symfony/translation": "^6.4|^7.0",
43+
"symfony/validator": "^6.4|^7.0",
4344
"symfony/yaml": "^6.4|^7.0",
4445
"symfony/security-acl": "^2.8|^3.0",
4546
"symfony/security-core": "^6.4|^7.0",

src/Symfony/Bundle/TwigBundle/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ CHANGELOG
44
7.3
55
---
66

7+
<<<<<<< HEAD
78
* Enable `#[AsTwigFilter]`, `#[AsTwigFunction]` and `#[AsTwigTest]` attributes
89
to configure extensions on runtime classes
10+
=======
11+
* Add support for a `twig` validator
12+
>>>>>>> dd76f094f4a (Add the twig constraint and its validator)
913
1014
7.1
1115
---

src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\Mailer\Mailer;
2626
use Symfony\Component\Translation\LocaleSwitcher;
2727
use Symfony\Component\Translation\Translator;
28+
use Symfony\Component\Validator\Constraint;
2829
use Symfony\Contracts\Service\ResetInterface;
2930
use Twig\Attribute\AsTwigFilter;
3031
use Twig\Attribute\AsTwigFunction;
@@ -69,6 +70,10 @@ public function load(array $configs, ContainerBuilder $container): void
6970
$container->removeDefinition('twig.translation.extractor');
7071
}
7172

73+
if ($container::willBeAvailable('symfony/validator', Constraint::class, ['symfony/twig-bundle'])) {
74+
$loader->load('validator.php');
75+
}
76+
7277
foreach ($configs as $key => $config) {
7378
if (isset($config['globals'])) {
7479
foreach ($config['globals'] as $name => $value) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Bridge\Twig\Validator\Constraints\TwigValidator;
15+
16+
return static function (ContainerConfigurator $container) {
17+
$container->services()
18+
->set('twig.validator', TwigValidator::class)
19+
->args([service('twig')])
20+
->tag('validator.constraint_validator')
21+
;
22+
};

0 commit comments

Comments
 (0)
0