Skip to content

Commit 752b41d

Browse files
committed
[TwigBundle] Improve error when autoconfiguring a class with both ExtensionInterface and Twig callable attribute
1 parent 0b4d21c commit 752b41d

File tree

2 files changed

+72
-15
lines changed

2 files changed

+72
-15
lines changed

src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/AttributeExtensionPass.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
use Symfony\Component\DependencyInjection\ChildDefinition;
1515
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1718
use Twig\Attribute\AsTwigFilter;
1819
use Twig\Attribute\AsTwigFunction;
1920
use Twig\Attribute\AsTwigTest;
21+
use Twig\Extension\AbstractExtension;
2022
use Twig\Extension\AttributeExtension;
23+
use Twig\Extension\ExtensionInterface;
2124

2225
/**
2326
* Register an instance of AttributeExtension for each service using the
@@ -33,6 +36,14 @@ final class AttributeExtensionPass implements CompilerPassInterface
3336

3437
public static function autoconfigureFromAttribute(ChildDefinition $definition, AsTwigFilter|AsTwigFunction|AsTwigTest $attribute, \ReflectionMethod $reflector): void
3538
{
39+
$class = $reflector->getDeclaringClass();
40+
if ($class->implementsInterface(ExtensionInterface::class)) {
41+
if ($class->isSubclassOf(AbstractExtension::class)) {
42+
throw new LogicException(\sprintf('The class "%s" cannot extend "%s" and use the "#[%s]" attribute on method "%s()", choose one or the other.', $class->name, AbstractExtension::class, $attribute::class, $reflector->name));
43+
}
44+
throw new LogicException(\sprintf('The class "%s" cannot implement "%s" and use the "#[%s]" attribute on method "%s()", choose one or the other.', $class->name, ExtensionInterface::class, $attribute::class, $reflector->name));
45+
}
46+
3647
$definition->addTag(self::TAG);
3748

3849
// The service must be tagged as a runtime to call non-static methods

src/Symfony/Bundle/TwigBundle/Tests/Functional/AttributeExtensionTest.php

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,39 @@
1111

1212
namespace Symfony\Bundle\TwigBundle\Tests\Functional;
1313

14+
use PHPUnit\Framework\Attributes\After;
15+
use PHPUnit\Framework\Attributes\Before;
16+
use PHPUnit\Framework\Attributes\BeforeClass;
1417
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
1518
use Symfony\Bundle\TwigBundle\Tests\TestCase;
1619
use Symfony\Bundle\TwigBundle\TwigBundle;
1720
use Symfony\Component\Config\Loader\LoaderInterface;
1821
use Symfony\Component\DependencyInjection\ContainerBuilder;
22+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1923
use Symfony\Component\Filesystem\Filesystem;
2024
use Symfony\Component\HttpKernel\Kernel;
2125
use Twig\Attribute\AsTwigFilter;
2226
use Twig\Attribute\AsTwigFunction;
2327
use Twig\Attribute\AsTwigTest;
2428
use Twig\Environment;
2529
use Twig\Error\RuntimeError;
30+
use Twig\Extension\AbstractExtension;
2631
use Twig\Extension\AttributeExtension;
2732

2833
class AttributeExtensionTest extends TestCase
2934
{
30-
public function testExtensionWithAttributes()
35+
/** @beforeClass */
36+
#[BeforeClass]
37+
public static function assertTwigVersion(): void
3138
{
3239
if (!class_exists(AttributeExtension::class)) {
3340
self::markTestSkipped('Twig 3.21 is required.');
3441
}
42+
}
3543

36-
$kernel = new class('test', true) extends Kernel
37-
{
38-
public function registerBundles(): iterable
39-
{
40-
return [new FrameworkBundle(), new TwigBundle()];
41-
}
42-
44+
public function testExtensionWithAttributes()
45+
{
46+
$kernel = new class extends AttributeExtensionKernel {
4347
public function registerContainerConfiguration(LoaderInterface $loader): void
4448
{
4549
$loader->load(static function (ContainerBuilder $container) {
@@ -53,11 +57,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
5357
$container->setAlias('twig_test', 'twig')->setPublic(true);
5458
});
5559
}
56-
57-
public function getProjectDir(): string
58-
{
59-
return sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension';
60-
}
6160
};
6261

6362
$kernel->boot();
@@ -73,10 +72,30 @@ public function getProjectDir(): string
7372
$twig->getRuntime(StaticExtensionWithAttributes::class);
7473
}
7574

75+
public function testInvalidExtensionClass()
76+
{
77+
$kernel = new class extends AttributeExtensionKernel {
78+
public function registerContainerConfiguration(LoaderInterface $loader): void
79+
{
80+
$loader->load(static function (ContainerBuilder $container) {
81+
$container->register(InvalidExtensionWithAttributes::class, InvalidExtensionWithAttributes::class)
82+
->setAutoconfigured(true);
83+
});
84+
}
85+
};
86+
87+
$this->expectException(LogicException::class);
88+
$this->expectExceptionMessage('The class "Symfony\Bundle\TwigBundle\Tests\Functional\InvalidExtensionWithAttributes" cannot extend "Twig\Extension\AbstractExtension" and use the "#[Twig\Attribute\AsTwigFilter]" attribute on method "funFilter()", choose one or the other.');
89+
90+
$kernel->boot();
91+
}
92+
93+
7694
/**
7795
* @before
7896
* @after
7997
*/
98+
#[Before, After]
8099
protected function deleteTempDir()
81100
{
82101
if (file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension')) {
@@ -85,6 +104,24 @@ protected function deleteTempDir()
85104
}
86105
}
87106

107+
abstract class AttributeExtensionKernel extends Kernel
108+
{
109+
public function __construct()
110+
{
111+
parent::__construct('test', true);
112+
}
113+
114+
public function registerBundles(): iterable
115+
{
116+
return [new FrameworkBundle(), new TwigBundle()];
117+
}
118+
119+
public function getProjectDir(): string
120+
{
121+
return sys_get_temp_dir().'/'.Kernel::VERSION.'/AttributeExtension';
122+
}
123+
}
124+
88125
class StaticExtensionWithAttributes
89126
{
90127
#[AsTwigFilter('foo')]
@@ -112,10 +149,19 @@ public function __construct(private bool $prefix)
112149
{
113150
}
114151

115-
#[AsTwigFilter('foo')]
116-
#[AsTwigFunction('foo')]
152+
#[AsTwigFilter('prefix_foo')]
153+
#[AsTwigFunction('prefix_foo')]
117154
public function prefix(string $value): string
118155
{
119156
return $this->prefix.$value;
120157
}
121158
}
159+
160+
class InvalidExtensionWithAttributes extends AbstractExtension
161+
{
162+
#[AsTwigFilter('fun')]
163+
public function funFilter(): string
164+
{
165+
return 'fun';
166+
}
167+
}

0 commit comments

Comments
 (0)