8000 [TwigBundle] Enable `#[AsTwigFilter]`, `#[AsTwigFunction]` and `#[AsT… · symfony/symfony@c8780d1 · GitHub
[go: up one dir, main page]

Skip to content

Commit c8780d1

Browse files
GromNaNfabpot
authored andcommitted
[TwigBundle] Enable #[AsTwigFilter], #[AsTwigFunction] and #[AsTwigTest] attributes to configure runtime extensions
1 parent 21b4dd7 commit c8780d1

File tree

5 files changed

+188
-0
lines changed

5 files changed

+188
-0
lines changed

src/Symfony/Bundle/TwigBundle/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Enable `#[AsTwigFilter]`, `#[AsTwigFunction]` and `#[AsTwigTest]` attributes
8+
to configure extensions on runtime classes
9+
410
7.1
511
---
612

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\Bundle\TwigBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ChildDefinition;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Twig\Attribute\AsTwigFilter;
18+
use Twig\Attribute\AsTwigFunction;
19+
use Twig\Attribute\AsTwigTest;
20+
use Twig\Extension\AttributeExtension;
21+
22+
/**
23+
* Register an instance of AttributeExtension for each service using the
24+
* PHP attributes to declare Twig callables.
25+
*
26+
* @author Jérôme Tamarelle <jerome@tamarelle.net>
27+
*
28+
* @internal
29+
*/
30+
final class AttributeExtensionPass implements CompilerPassInterface
31+
{
32+
private const TAG = 'twig.attribute_extension';
33+
34+
public static function autoconfigureFromAttribute(ChildDefinition $definition, AsTwigFilter|AsTwigFunction|AsTwigTest $attribute, \ReflectionMethod $reflector): void
35+
{
36+
$definition->addTag(self::TAG);
37+
38+
// The service must be tagged as a runtime to call non-static methods
39+
if (!$reflector->isStatic()) {
40+
$definition->addTag('twig.runtime');
41+
}
42+
}
43+
44+
public function process(ContainerBuilder $container): void
45+
{
46+
foreach ($container->findTaggedServiceIds(self::TAG, true) as $id => $tags) {
47+
$container->register('.twig.extension.'.$id, AttributeExtension::class)
48+
->setArguments([$container->getDefinition($id)->getClass()])
49+
->addTag('twig.extension');
50+
}
51+
}
52+
}

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

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Bundle\TwigBundle\DependencyInjection;
1313

14+
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\AttributeExtensionPass;
1415
use Symfony\Component\AssetMapper\AssetMapper;
1516
use Symfony\Component\Config\FileLocator;
1617
use Symfony\Component\Config\Resource\FileExistenceResource;
@@ -25,6 +26,9 @@
2526
use Symfony\Component\Translation\LocaleSwitcher;
2627
use Symfony\Component\Translation\Translator;
2728
use Symfony\Contracts\Service\ResetInterface;
29+
use Twig\Attribute\AsTwigFilter;
30+
use Twig\Attribute\AsTwigFunction;
31+
use Twig\Attribute\AsTwigTest;
2832
use Twig\Environment;
2933
use Twig\Extension\ExtensionInterface;
3034
use Twig\Extension\RuntimeExtensionInterface;
@@ -179,6 +183,10 @@ public function load(array $configs, ContainerBuilder $container): void
179183
$container->registerForAutoconfiguration(LoaderInterface::class)->addTag('twig.loader');
180184
$container->registerForAutoconfiguration(RuntimeExtensionInterface::class)->addTag('twig.runtime');
181185

186+
$container->registerAttributeForAutoconfiguration(AsTwigFilter::class, AttributeExtensionPass::autoconfigureFromAttribute(...));
187+
$container->registerAttributeForAutoconfiguration(AsTwigFunction::class, AttributeExtensionPass::autoconfigureFromAttribute(...));
188+
$container->registerAttributeForAutoconfiguration(AsTwigTest::class, AttributeExtensionPass::autoconfigureFromAttribute(...));
189+
182190
if (false === $config['cache']) {
183191
$container->removeDefinition('twig.template_cache_warmer');
184192
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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\Bundle\TwigBundle\Tests\Functional;
13+
14+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
15+
use Symfony\Bundle\TwigBundle\Tests\TestCase;
16+
use Symfony\Bundle\TwigBundle\TwigBundle;
17+
use Symfony\Component\Config\Loader\LoaderInterface;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\Filesystem\Filesystem;
20+
use Symfony\Component\HttpKernel\Kernel;
21+
use Twig\Attribute\AsTwigFilter;
22+
use Twig\Attribute\AsTwigFunction;
23+
use Twig\Attribute\AsTwigTest;
24+
use Twig\Environment;
25+
use Twig\Error\RuntimeError;
26+
use Twig\Extension\AttributeExtension;
27+
28+
class AttributeExtensionTest extends TestCase
29+
{
30+
public function testExtensionWithAttributes()
31+
{
32+
if (!class_exists(AttributeExtension::class)) {
33+
self::markTestSkipped('Twig 3.21 is required.');
34+
}
35+
36+
$kernel = new class('test', true) extends Kernel
37+
{
38+
public function registerBundles(): iterable
39+
{
40+
return [new FrameworkBundle(), new TwigBundle()];
41+
}
42+
43+
public function registerContainerConfiguration(LoaderInterface $loader): void
44+
{
45+
$loader->load(static function (ContainerBuilder $container) {
46+
$container->register(StaticExtensionWithAttributes::class, StaticExtensionWithAttributes::class)
47+
->setAutoconfigured(true);
48+
$container->register(RuntimeExtensionWithAttributes::class, RuntimeExtensionWithAttributes::class)
49+
->setArguments(['prefix_'])
50+
->setAutoconfigured(true);
51+
52+
$container->setAlias('twig_test', 'twig')->setPublic(true);
53+
});
54+
}
55+
56+
public function getProjectDir(): string
57+
{
58+
return sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension';
59+
}
60+
};
61+
62+
$kernel->boot();
63+
64+
/** @var Environment $twig */
65+
$twig = $kernel->getContainer()->get('twig_test');
66+
67+
self::assertInstanceOf(AttributeExtension::class, $twig->getExtension(StaticExtensionWithAttributes::class));
68+
self::assertInstanceOf(AttributeExtension::class, $twig->getExtension(RuntimeExtensionWithAttributes::class));
69+
self::assertInstanceOf(RuntimeExtensionWithAttributes::class, $twig->getRuntime(RuntimeExtensionWithAttributes::class));
70+
71+
self::expectException(RuntimeError::class);
72+
$twig->getRuntime(StaticExtensionWithAttributes::class);
73+
}
74+
75+
/**
76+
* @before
77+
* @after
78+
*/
79+
protected function deleteTempDir()
80+
{
81+
if (file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension')) {
82+
(new Filesystem())->remove($dir);
83+
}
84+
}
85+
}
86+
87+
class StaticExtensionWithAttributes
88+
{
89+
#[AsTwigFilter('foo')]
90+
public static function fooFilter(string $value): string
91+
{
92+
return $value;
93+
}
94+
95+
#[AsTwigFunction('foo')]
96+
public static function fooFunction(string $value): string
97+
{
98+
return $value;
99+
}
100+
101+
#[AsTwigTest('foo')]
102+
public static function fooTest(bool $value): bool
103+
{
104+
return $value;
105+
}
106+
}
107+
108+
class RuntimeExtensionWithAttributes
109+
{
110+
public function __construct(private bool $prefix)
111+
{
112+
}
113+
114+
#[AsTwigFilter('foo')]
115+
#[AsTwigFunction('foo')]
116+
public function prefix(string $value): string
117+
{
118+
return $this->prefix.$value;
119+
}
120+
}

src/Symfony/Bundle/TwigBundle/TwigBundle.php

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Bundle\TwigBundle;
1313

14+
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\AttributeExtensionPass;
1415
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExtensionPass;
1516
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass;
1617
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigEnvironmentPass;
@@ -33,6 +34,7 @@ public function build(ContainerBuilder $container): void
3334

3435
// ExtensionPass must be run before the FragmentRendererPass as it adds tags that are processed later
3536
$container->addCompilerPass(new ExtensionPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
37+
$container->addCompilerPass(new AttributeExtensionPass());
3638
$container->addCompilerPass(new TwigEnvironmentPass());
3739
$container->addCompilerPass(new TwigLoaderPass());
3840
$container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING);

0 commit comments

Comments
 (0)
0