Skip to content

Navigation Menu

Sign in
Appearance settings

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 a05b6a3

Browse filesBrowse files
committed
feature #42039 [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters (ruudk)
This PR was merged into the 5.4 branch. Discussion ---------- [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? |no | New feature? | yes | Deprecations? |no | Tickets | | License | MIT | Doc PR | ## Introduction #39897 introduced the possibility auto configure classes that were annotated with attributes: ```php $container->registerAttributeForAutoconfiguration( MyAttribute::class, static function (ChildDefinition $definition, MyAttribute $attribute, \ReflectionClass $reflector): void { $definition->addTag('my_tag', ['some_property' => $attribute->someProperty]); } ); ``` This works great. But it only works when the attribute is added on the class. With this PR, it's now possible to also auto configure methods, properties and parameters. ## How does it work? Let's say you have an attribute that targets classes and methods like this: ```php #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)] final class MyAttribute { } ``` You have two services that use them: ```php #[MyAttribute] class MyService { } class MyOtherService { #[MyAttribute] public function myMethod() {} } ``` You can now use `registerAttributeForAutoconfiguration` in your extension, together with a union of the types that you want to seach for. In this example, the extension only cares for classes and methods, so it uses `\ReflectionClass|\ReflectionMethod $reflector`: ```php final class MyBundleExtension extends Extension { public function load(array $configs, ContainerBuilder $container) : void { $container->registerAttributeForAutoconfiguration( MyAttribute::class, static function (ChildDefinition $definition, MyAttribute $attribute, \ReflectionClass|\ReflectionMethod $reflector) : void { $args = []; if ($reflector instanceof \ReflectionMethod) { $args['method'] = $reflector->getName(); } $definition->addTag('my.tag', $args); } ); } } ``` This will tag `MyService` with `my.tag` and it will tag `MyOtherService` with `my.tag, method: myMethod` If the extension also wants to target the properties that are annotated with attributes, it can either change the union to `\ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector` or it can just use `\Reflector $reflector` and do the switching in the callable. ## Another example Let's say you have an attribute like this: ```php #[Attribute(Attribute::TARGET_CLASS)] final class MyAttribute { } ``` and you use it like this: ```php $container->registerAttributeForAutoconfiguration( MyAttribute::class, static function (ChildDefinition $definition, MyAttribute $attribute, \ReflectionClass|\ReflectionMethod $reflector) : void { $definition->addTag('my.tag'); } ); ``` you'll get an error saying that `ReflectionMethod` is not possible as the attribute only targets classes. Commits ------- 917fcc0 [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters
2 parents af1a5be + 917fcc0 commit a05b6a3
Copy full SHA for a05b6a3

File tree

15 files changed

+390
-16
lines changed
Filter options

15 files changed

+390
-16
lines changed

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

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

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

‎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, properties and parameters
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
+9-1Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,15 @@ 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+
* The third argument passed to the callable is the reflector of the
1313+
* class/method/property/parameter that the attribute targets. Using one or many of
1314+
* \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter as a type-hint
1315+
* for this argument allows filtering which attributes should be passed to the callable.
1316+
*
1317+
* @template T
1318+
*
1319+
* @param class-string<T> $attributeClass
1320+
* @param callable(ChildDefinition, T, \Reflector): void $configurator
13131321
*/
13141322
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
13151323
{

‎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.