Skip to content

Commit 044faf5

Browse files
ruudknicolas-grekas
authored andcommitted
[DependencyInjection] Autoconfigurable attributes on methods, properties and parameters
1 parent 2737fcb commit 044faf5

10 files changed

+351
-10
lines changed

CHANGELOG.md

Lines 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
---

Compiler/AttributeAutoconfigurationPass.php

Lines 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 <[email protected]>
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

ContainerBuilder.php

Lines 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
{

Tests/Compiler/AttributeAutoconfigurationPassTest.php

Lines 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
}

Tests/Compiler/IntegrationTest.php

Lines 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
*/
Lines 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 <[email protected]>
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+
}
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 <[email protected]>
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+
}
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 <[email protected]>
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+
}
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 <[email protected]>
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_PROPERTY)]
15+
final class CustomPropertyAttribute
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}

0 commit comments

Comments
 (0)