Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6911e60

Browse filesBrowse files
ruudknicolas-grekas
authored andcommitted
[DependencyInjection] Autoconfigurable attributes on methods, properties and parameters
1 parent 0bf0278 commit 6911e60
Copy full SHA for 6911e60

File tree

15 files changed

+393
-17
lines changed
Filter options

15 files changed

+393
-17
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+13-3Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,19 @@ public function load(array $configs, ContainerBuilder $container)
555555
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
556556
->addMethodCall('setLogger', [new Reference('logger')]);
557557

558-
$container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute): void {
559-
$definition->addTag('kernel.event_listener', get_object_vars($attribute));
560-
});
558+
if (\PHP_VERSION_ID >= 80000) {
559+
$container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \Reflector $reflector) {
560+
$tagAttributes = get_object_vars($attribute);
561+
if ($reflector instanceof \ReflectionMethod) {
562+
if (isset($tagAttributes['method'])) {
563+
throw new LogicException(sprintf('AsEventListener attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name));
564+
}
565+
$tagAttributes['method'] = $reflector->getName();
566+
}
567+
$definition->addTag('kernel.event_listener', $tagAttributes);
568+
});
569+
}
570+
561571
$container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void {
562572
$definition->addTag('controller.service_arguments');
563573
});

‎src/Symfony/Component/DependencyInjection/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ CHANGELOG
44
5.4
55
---
66

7-
* Add `service_closure()` to the PHP-DSL
7+
* Add `service_closure()` to the PHP-DSL
8+
* Add support for autoconfigurable attributes on methods
89

910
5.3
1011
---

‎src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php
+109-8Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,66 @@
1414
use Symfony\Component\DependencyInjection\ChildDefinition;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1718

1819
/**
1920
* @author Alexander M. Turek <me@derrabus.de>
2021
*/
2122
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
2223
{
24+
private $classAttributeConfigurators = [];
25+
private $methodAttributeConfigurators = [];
26+
private $propertyAttributeConfigurators = [];
27+
private $parameterAttributeConfigurators = [];
28+
2329
public function process(ContainerBuilder $container): void
2430
{
2531
if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) {
2632
return;
2733
}
2834

35+
foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) {
36+
$callableReflector = new \ReflectionFunction(\Closure::fromCallable($callable));
37+
if ($callableReflector->getNumberOfParameters() <= 2) {
38+
$this->classAttributeConfigurators[$attributeName] = $callable;
39+
continue;
40+
}
41+
42+
$reflectorParameter = $callableReflector->getParameters()[2];
43+
$parameterType = $reflectorParameter->getType();
44+
$types = [];
45+
if ($parameterType instanceof \ReflectionUnionType) {
46+
foreach ($parameterType->getTypes() as $type) {
47+
$types[] = $type->getName();
48+
}
49+
} elseif ($parameterType instanceof \ReflectionNamedType) {
50+
$types[] = $parameterType->getName();
51+
} else {
52+
throw new LogicException(sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine()));
53+
}
54+
55+
try {
56+
$attributeReflector = new \ReflectionClass($attributeName);
57+
} catch (\ReflectionException $e) {
58+
continue;
59+
}
60+
61+
$targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0;
62+
$targets = $targets ? $targets->getArguments()[0] ?? -1 : 0;
63+
64+
foreach (['class', 'method', 'property', 'parameter'] as $symbol) {
65+
if (['Reflector'] !== $types) {
66+
if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) {
67+
continue;
68+
}
69+
if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) {
70+
throw new LogicException(sprintf('Invalid type "Reflection%s" on argument "$%s": attribute "%s" cannot target a '.$symbol.' in "%s" on line "%d".', ucfirst($symbol), $reflectorParameter->getName(), $attributeName, $callableReflector->getFileName(), $callableReflector->getStartLine()));
71+
}
72+
}
73+
$this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable;
74+
}
75+
}
76+
2977
parent::process($container);
3078
}
3179

@@ -35,21 +83,74 @@ protected function processValue($value, bool $isRoot = false)
3583
|| !$value->isAutoconfigured()
3684
|| $value->isAbstract()
3785
|| $value->hasTag('container.ignore_attributes')
38-
|| !($reflector = $this->container->getReflectionClass($value->getClass(), false))
86+
|| !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
3987
) {
4088
return parent::processValue($value, $isRoot);
4189
}
4290

43-
$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
4491
$instanceof = $value->getInstanceofConditionals();
45-
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
46-
foreach ($reflector->getAttributes() as $attribute) {
47-
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
48-
$configurator($conditionals, $attribute->newInstance(), $reflector);
92+
$conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition('');
93+
94+
if ($this->classAttributeConfigurators) {
95+
foreach ($classReflector->getAttributes() as $attribute) {
96+
if ($configurator = $this->classAttributeConfigurators[$attribute->getName()] ?? null) {
97+
$configurator($conditionals, $attribute->newInstance(), $classReflector);
98+
}
4999
}
50100
}
51-
if (!isset($instanceof[$reflector->getName()]) && new ChildDefinition('') != $conditionals) {
52-
$instanceof[$reflector->getName()] = $conditionals;
101+
102+
if ($this->parameterAttributeConfigurators && $constructorReflector = $this->getConstructor($value, false)) {
103+
foreach ($constructorReflector->getParameters() as $parameterReflector) {
104+
foreach ($parameterReflector->getAttributes() as $attribute) {
105+
if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
106+
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
107+
}
108+
}
109+
}
110+
}
111+
112+
if ($this->methodAttributeConfigurators || $this->parameterAttributeConfigurators) {
113+
foreach ($classReflector->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflector) {
114+
if ($methodReflector->isStatic() || $methodReflector->isConstructor() || $methodReflector->isDestructor()) {
115+
continue;
116+
}
117+
118+
if ($this->methodAttributeConfigurators) {
119+
foreach ($methodReflector->getAttributes() as $attribute) {
120+
if ($configurator = $this->methodAttributeConfigurators[$attribute->getName()] ?? null) {
121+
$configurator($conditionals, $attribute->newInstance(), $methodReflector);
122+
}
123+
}
124+
}
125+
126+
if ($this->parameterAttributeConfigurators) {
127+
foreach ($methodReflector->getParameters() as $parameterReflector) {
128+
foreach ($parameterReflector->getAttributes() as $attribute) {
129+
if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
130+
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
131+
}
132+
}
133+
}
134+
}
135+
}
136+
}
137+
138+
if ($this->propertyAttributeConfigurators) {
139+
foreach ($classReflector->getProperties(\ReflectionProperty::IS_PUBLIC) as $propertyReflector) {
140+
if ($propertyReflector->isStatic()) {
141+
continue;
142+
}
143+
144+
foreach ($propertyReflector->getAttributes() as $attribute) {
145+
if ($configurator = $this->propertyAttributeConfigurators[$attribute->getName()] ?? null) {
146+
$configurator($conditionals, $attribute->newInstance(), $propertyReflector);
147+
}
148+
}
149+
}
150+
}
151+
152+
if (!isset($instanceof[$classReflector->getName()]) && new ChildDefinition('') != $conditionals) {
153+
$instanceof[$classReflector->getName()] = $conditionals;
53154
$value->setInstanceofConditionals($instanceof);
54155
}
55156

‎src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,14 @@ public function registerForAutoconfiguration(string $interface)
13091309
/**
13101310
* Registers an attribute that will be used for autoconfiguring annotated classes.
13111311
*
1312-
* The configurator will receive a ChildDefinition instance, an instance of the attribute and the corresponding \ReflectionClass, in that order.
1312+
* If an attribute can only be used on 1 target, the \Reflector type should be narrowed to
1313+
* one of: \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter
1314+
*
1315+
* If an attribute can be used on multiple targets, a union type can be used based on above types.
1316+
*
1317+
* @template T
1318+
* @param class-string<T> $attributeClass
1319+
* @param callable(ChildDefinition $definition, T $attribute, \Reflector $reflector): void $configurator
13131320
*/
13141321
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
13151322
{

‎src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php
+15Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
16+
use Symfony\Component\DependencyInjection\ChildDefinition;
1617
use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass;
1718
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1820

1921
/**
2022
* @requires PHP 8
@@ -33,4 +35,17 @@ public function testProcessAddsNoEmptyInstanceofConditionals()
3335

3436
$this->assertSame([], $container->getDefinition('foo')->getInstanceofConditionals());
3537
}
38+
39+
public function testAttributeConfiguratorCallableMissingType()
40+
{
41+
$container = new ContainerBuilder();
42+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, static function (ChildDefinition $definition, AsTaggedItem $attribute, $reflector) {});
43+
$container->register('foo', \stdClass::class)
44+
->setAutoconfigured(true)
45+
;
46+
47+
$this->expectException(LogicException::class);
48+
$this->expectExceptionMessage('Argument "$reflector" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in ');
49+
(new AttributeAutoconfigurationPass())->process($container);
50+
}
3651
}

‎src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php
+85Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
2424
use Symfony\Component\DependencyInjection\Reference;
2525
use Symfony\Component\DependencyInjection\ServiceLocator;
26+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute;
2627
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAutoconfiguration;
28+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomMethodAttribute;
29+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomParameterAttribute;
30+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomPropertyAttribute;
2731
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
2832
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
2933
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
@@ -37,6 +41,7 @@
3741
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
3842
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
3943
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3Configurator;
44+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService4;
4045
use Symfony\Contracts\Service\ServiceProviderInterface;
4146
use Symfony\Contracts\Service\ServiceSubscriberInterface;
4247

@@ -729,6 +734,86 @@ static function (Definition $definition, CustomAutoconfiguration $attribute) {
729734
], $collector->collectedTags);
730735
}
731736

737+
/**
738+
* @requires PHP 8
739+
*/
740+
public function testTagsViaAttributeOnPropertyMethodAndParameter()
741+
{
742+
$container = new ContainerBuilder();
743+
$container->registerAttributeForAutoconfiguration(
744+
CustomMethodAttribute::class,
745+
static function (ChildDefinition $definition, CustomMethodAttribute $attribute, \ReflectionMethod $reflector) {
746+
$tagAttributes = get_object_vars($attribute);
747+
$tagAttributes['method'] = $reflector->getName();
748+
749+
$definition->addTag('app.custom_tag', $tagAttributes);
750+
}
751+
);
752+
$container->registerAttributeForAutoconfiguration(
753+
CustomPropertyAttribute::class,
754+
static function (ChildDefinition $definition, CustomPropertyAttribute $attribute, \ReflectionProperty $reflector) {
755+
$tagAttributes = get_object_vars($attribute);
756+
$tagAttributes['property'] = $reflector->getName();
757+
758+
$definition->addTag('app.custom_tag', $tagAttributes);
759+
}
760+
);
761+
$container->registerAttributeForAutoconfiguration(
762+
CustomParameterAttribute::class,
763+
static function (ChildDefinition $definition, CustomParameterAttribute $attribute, \ReflectionParameter $reflector) {
764+
$tagAttributes = get_object_vars($attribute);
765+
$tagAttributes['parameter'] = $reflector->getName();
766+
767+
$definition->addTag('app.custom_tag', $tagAttributes);
768+
}
769+
);
770+
$container->registerAttributeForAutoconfiguration(
771+
CustomAnyAttribute::class,
772+
eval(<<<'PHP'
773+
return static function (\Symfony\Component\DependencyInjection\ChildDefinition $definition, \Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $reflector) {
774+
$tagAttributes = get_object_vars($attribute);
775+
if ($reflector instanceof \ReflectionClass) {
776+
$tagAttributes['class'] = $reflector->getName();
777+
} elseif ($reflector instanceof \ReflectionMethod) {
778+
$tagAttributes['method'] = $reflector->getName();
779+
} elseif ($reflector instanceof \ReflectionProperty) {
780+
$tagAttributes['property'] = $reflector->getName();
781+
} elseif ($reflector instanceof \ReflectionParameter) {
782+
$tagAttributes['parameter'] = $reflector->getName();
783+
}
784+
785+
$definition->addTag('app.custom_tag', $tagAttributes);
786+
};
787+
PHP
788+
));
789+
790+
$container->register(TaggedService4::class)
791+
->setPublic(true)
792+
->setAutoconfigured(true);
793+
794+
$collector = new TagCollector();
795+
$container->addCompilerPass($collector);
796+
797+
$container->compile();
798+
799+
self::assertSame([
800+
TaggedService4::class => [
801+
['class' => TaggedService4::class],
802+
['parameter' => 'param1'],
803+
['someAttribute' => 'on param1 in constructor', 'priority' => 0, 'parameter' => 'param1'],
804+
['parameter' => 'param2'],
805+
['someAttribute' => 'on param2 in constructor', 'priority' => 0, 'parameter' => 'param2'],
806+
['method' => 'fooAction'],
807+
['someAttribute' => 'on fooAction', 'priority' => 0, 'method' => 'fooAction'],
808+
['someAttribute' => 'on param1 in fooAction', 'priority' => 0, 'parameter' => 'param1'],
809+
['method' => 'barAction'],
810+
['someAttribute' => 'on barAction', 'priority' => 0, 'method' => 'barAction'],
811+
['property' => 'name'],
812+
['someAttribute' => 'on name', 'priority' => 0, 'property' => 'name'],
813+
],
814+
], $collector->collectedTags);
815+
}
816+
732817
/**
733818
* @requires PHP 8
734819
*/
+17Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\Tests\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
15+
final class CustomAnyAttribute
16+
{
17+
}
+22Lines 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\Tests\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_METHOD)]
15+
final class CustomMethodAttribute
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}
+22Lines 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\Tests\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15+
final class CustomParameterAttribute
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.