8000 feature #60011 [DependencyInjection] Enable multiple attribute autoco… · symfony/symfony@caa6d0e · GitHub
[go: up one dir, main page]

Skip to content

Commit caa6d0e

Browse files
feature #60011 [DependencyInjection] Enable multiple attribute autoconfiguration callbacks on the same class (GromNaN)
This PR was squashed before being merged into the 7.3 branch. Discussion ---------- [DependencyInjection] Enable multiple attribute autoconfiguration callbacks on the same class | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | yes | Deprecations? | yes | Issues | Fix doctrine/DoctrineBundle#1868 (comment) | License | MIT Replace #60001 By having a list of callables for each attributes, we can enable merging definitions each having an autoconfiguration for the same attribute class. This is the case with the `#[Entity]` attribute in DoctrineBundle and FrameworkBundle. I have to deprecate `ContainerBuilder::getAutoconfiguredAttributes()` as its return type is `array<class-string, callable>`; so I added a new method `AttributeAutoconfigurationPass` that returns `array<class-string, callable[]>` in in order to use reflection on each callable in the compiler pass. Commits ------- e36fe60 [DependencyInjection] Enable multiple attribute autoconfiguration callbacks on the same class
2 parents baf2067 + e36fe60 commit caa6d0e

File tree

5 files changed

+129
-77
lines changed

5 files changed

+129
-77
lines changed

UPGRADE-7.3.md

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ Console
3939

4040
* Deprecate methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute
41 8000 41

42+
DependencyInjection
43+
-------------------
44+
45+
* Deprecate `ContainerBuilder::getAutoconfiguredAttributes()` in favor of the `getAttributeAutoconfigurators()` method.
46+
4247
FrameworkBundle
4348
---------------
4449

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Don't skip classes with private constructor when autodiscovering
1010
* Add `Definition::addResourceTag()` and `ContainerBuilder::findTaggedResourceIds()`
1111
for auto-configuration of classes excluded from the service container
12+
* Accept multiple auto-configuration callbacks for the same attribute class
1213
* Leverage native lazy objects when possible for lazy services
1314

1415
7.2

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

+62-68
Original file line numberDiff line numberDiff line change
@@ -31,49 +31,51 @@ final class AttributeAutoconfigurationPass extends AbstractRecursivePass
3131

3232
public function process(ContainerBuilder $container): void
3333
{
34-
if (!$container->getAutoconfiguredAttributes()) {
34+
if (!$container->getAttributeAutoconfigurators()) {
3535
return;
3636
}
3737

38-
foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) {
39-
$callableReflector = new \ReflectionFunction($callable(...));
40-
if ($callableReflector->getNumberOfParameters() <= 2) {
41-
$this->classAttributeConfigurators[$attributeName] = $callable;
42-
continue;
43-
}
38+
foreach ($container->getAttributeAutoconfigurators() as $attributeName => $callables) {
39+
foreach ($callables as $callable) {
40+
$callableReflector = new \ReflectionFunction($callable(...));
41+
if ($callableReflector->getNumberOfParameters() <= 2) {
42+
$this->classAttributeConfigurators[$attributeName][] = $callable;
43+
continue;
44+
}
4445

45-
$reflectorParameter = $callableReflector->getParameters()[2];
46-
$parameterType = $reflectorParameter->getType();
47-
$types = [];
48-
if ($parameterType instanceof \ReflectionUnionType) {
49-
foreach ($parameterType->getTypes() as $type) {
50-
$types[] = $type->getName();
46+
$reflectorParameter = $callableReflector->getParameters()[2];
47+
$parameterType = $reflectorParameter->getType();
48+
$types = [];
49+
if ($parameterType instanceof \ReflectionUnionType) {
50+
foreach ($parameterType->getTypes() as $type) {
51+
$types[] = $type->getName();
52+
}
53+
} elseif ($parameterType instanceof \ReflectionNamedType) {
54+
$types[] = $parameterType->getName();
55+
} else {
56+
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()));
5157
}
52-
} elseif ($parameterType instanceof \ReflectionNamedType) {
53-
$types[] = $parameterType->getName();
54-
} else {
55-
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()));
56-
}
5758

58-
try {
59-
$attributeReflector = new \ReflectionClass($attributeName);
60-
} catch (\ReflectionException) {
61-
continue;
62-
}
59+
try {
60+
$attributeReflector = new \ReflectionClass($attributeName);
61+
} catch (\ReflectionException) {
62+
continue;
63+
}
6364

64-
$targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0;
65-
$targets = $targets ? $targets->getArguments()[0] ?? -1 : 0;
65+
$targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0;
66+
$targets = $targets ? $targets->getArguments()[0] ?? -1 : 0;
6667

67-
foreach (['class', 'method', 'property', 'parameter'] as $symbol) {
68-
if (['Reflector'] !== $types) {
69-
if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) {
70-
continue;
71-
}
72-
if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) {
73-
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()));
68+
foreach (['class', 'method', 'property', 'parameter'] as $symbol) {
69+
if (['Reflector'] !== $types) {
70+
if (!\in_array('Reflection' . ucfirst($symbol), $types, true)) {
71+
continue;
72+
}
73+
if (!($targets & \constant('Attribute::TARGET_' . strtoupper($symbol)))) {
74+
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()));
75+
}
7476
}
77+
$this->{$symbol . 'AttributeConfigurators'}[$attributeName][] = $callable;
7578
}
76-
$this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable;
7779
}
7880
}
7981

@@ -94,13 +96,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
9496
$instanceof = $value->getInstanceofConditionals();
9597
$conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition('');
9698

97-
if ($this->classAttributeConfigurators) {
98-
foreach ($classReflector->getAttributes() as $attribute) {
99-
if ($configurator = $this->findConfigurator($this->classAttributeConfigurators, $attribute->getName())) {
100-
$configurator($conditionals, $attribute->newInstance(), $classReflector);
101-
}
102-
}
103-
}
99+
$this->callConfigurators($this->classAttributeConfigurators, $conditionals, $classReflector);
104100

105101
if ($this->parameterAttributeConfigurators) {
106102
try {
@@ -111,11 +107,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
111107

112108
if ($constructorReflector) {
113109
foreach ($constructorReflector->getParameters() as $parameterReflector) {
114-
foreach ($parameterReflector->getAttributes() as $attribute) {
115-
if ($configurator = $this->findConfigurator($this->parameterAttributeConfigurators, $attribute->getName())) {
116-
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
117-
}
118-
}
110+
$this->callConfigurators($this->parameterAttributeConfigurators, $conditionals, $parameterReflector);
119111
}
120112
}
121113
}
@@ -126,22 +118,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
126118
continue;
127119
}
128120

129-
if ($this->methodAttributeConfigurators) {
130-
foreach ($methodReflector->getAttributes() as $attribute) {
131-
if ($configurator = $this->findConfigurator($this->methodAttributeConfigurators, $attribute->getName())) {
132-
$configurator($conditionals, $attribute->newInstance(), $methodReflector);
133-
}
134-
}
135-
}
121+
$this->callConfigurators($this->methodAttributeConfigurators, $conditionals, $methodReflector);
136122

137-
if ($this->parameterAttributeConfigurators) {
138-
foreach ($methodReflector->getParameters() as $parameterReflector) {
139-
foreach ($parameterReflector->getAttributes() as $attribute) {
140-
if ($configurator = $this->findConfigurator($this->parameterAttributeConfigurators, $attribute->getName())) {
141-
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
142-
}
143-
}
144-
}
123+
foreach ($methodReflector->getParameters() as $parameterReflector) {
124+
$this->callConfigurators($this->parameterAttributeConfigurators, $conditionals, $parameterReflector);
145125
}
146126
}
147127
}
@@ -152,11 +132,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
152132
continue;
153133
}
154134

155-
foreach ($propertyReflector->getAttributes() as $attribute) {
156-
if ($configurator = $this->findConfigurator($this->propertyAttributeConfigurators, $attribute->getName())) {
157-
$configurator($conditionals, $attribute->newInstance(), $propertyReflector);
158-
}
159-
}
135+
$this->callConfigurators($this->propertyAttributeConfigurators, $conditionals, $propertyReflector);
160136
}
161137
}
162138

@@ -168,19 +144,37 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
168144
return parent::processValue($value, $isRoot);
169145
}
170146

147+
/**
148+
* Call all the configurators for the given attribute.
149+
*
150+
* @param array<class-string, callable[]> $configurators
151+
*/
152+
private function callConfigurators(array &$configurators, ChildDefinition $conditionals, \ReflectionClass|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflector): void
153+
{
154+
if (!$configurators) {
155+
return;
156+
}
157+
158+
foreach ($reflector->getAttributes() as $attribute) {
159+
foreach ($this->findConfigurators($configurators, $attribute->getName()) as $configurator) {
160+
$configurator($conditionals, $attribute->newInstance(), $reflector);
161+
}
162+
}
163+
}
164+
171165
/**
172166
* Find the first configurator for the given attribute name, looking up the class hierarchy.
173167
*/
174-
private function findConfigurator(array &$configurators, string $attributeName): ?callable
168+
private function findConfigurators(array &$configurators, string $attributeName): array
175169
{
176170
if (\array_key_exists($attributeName, $configurators)) {
177171
return $configurators[$attributeName];
178172
}
179173

180174
if (class_exists($attributeName) && $parent = get_parent_class($attributeName)) {
181-
return $configurators[$attributeName] = self::findConfigurator($configurators, $parent);
175+
return $configurators[$attributeName] = $this->findConfigurators($configurators, $parent);
182176
}
183177

184-
return $configurators[$attributeName] = null;
178+
return $configurators[$attributeName] = [];
185179
}
186180
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+29-9
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
129129
private array $autoconfiguredInstanceof = [];
130130

131131
/**
132-
* @var array<string, callable>
132+
* @var array<string, callable[]>
133133
*/
134134
private array $autoconfiguredAttributes = [];
135135

@@ -717,12 +717,11 @@ public function merge(self $container): void
717717
$this->autoconfiguredInstanceof[$interface] = $childDefinition;
718718
}
719719

720-
foreach ($container->getAutoconfiguredAttributes() as $attribute => $configurator) {
721-
if (isset($this->autoconfiguredAttributes[$attribute])) {
722-
throw new InvalidArgumentException(\sprintf('"%s" has already been autoconfigured and merge() does not support merging autoconfiguration for the same attribute.', $attribute));
723-
}
724-
725-
$this->autoconfiguredAttributes[$attribute] = $configurator;
720+
foreach ($container->getAttributeAutoconfigurators() as $attribute => $configurators) {
721+
$this->autoconfiguredAttributes[$attribute] = array_merge(
722+
$this->autoconfiguredAttributes[$attribute] ?? [],
723+
$configurators)
724+
;
726725
}
727726
}
728727

@@ -1448,7 +1447,7 @@ public function registerForAutoconfiguration(string $interface): ChildDefinition
14481447
*/
14491448
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
14501449
{
1451-
$this->autoconfiguredAttributes[$attributeClass] = $configurator;
1450+
$this->autoconfiguredAttributes[$attributeClass][] = $configurator;
14521451
}
14531452

14541453
/**
@@ -1489,9 +1488,30 @@ public function getAutoconfiguredInstanceof(): array
14891488
}
14901489

14911490
/**
1492-
* @return array<string, callable>
1491+
* @return array<class-string, callable>
1492+
*
1493+
* @deprecated Use {@see getAttributeAutoconfigurators()} instead
14931494
*/
14941495
public function getAutoconfiguredAttributes(): array
1496+
{
1497+
trigger_deprecation('symfony/dependency-injection', '7.3', 'The "%s()" method is deprecated, use "getAttributeAutoconfigurators()" instead.', __METHOD__);
1498+
1499+
$autoconfiguredAttributes = [];
1500+
foreach ($this->autoconfiguredAttributes as $attribute => $configurators) {
1501+
if (count($configurators) > 1) {
1502+
throw new LogicException(\sprintf('The "%s" attribute has %d configurators. Use "getAttributeAutoconfigurators()" to get all of them.', $attribute, count($configurators)));
1503+
}
1504+
1505+
$autoconfiguredAttributes[$attribute] = $configurators[0];
1506+
}
1507+
1508+
return $autoconfiguredAttributes;
1509+
}
1510+
1511+
/**
1512+
* @return array<class-string, callable[]>
1513+
*/
1514+
public function getAttributeAutoconfigurators(): array
14951515
{
14961516
return $this->autoconfiguredAttributes;
14971517
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

+32
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
2626
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
2727
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
28+
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
2829
use Symfony\Component\DependencyInjection\ChildDefinition;
2930
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
3031
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -34,6 +35,7 @@
3435
use Symfony\Component\DependencyInjection\Exception\BadMethodCallException;
3536
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
3637
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
38+
use Symfony\Component\DependencyInjection\Exception\LogicException;
3739
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
3840
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
3941
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@@ -829,6 +831,36 @@ public function testMergeThrowsExceptionForDuplicateAutomaticInstanceofDefinitio
829831
$container->merge($config);
830832
}
831833

834+
public function testMergeAttributeAutoconfiguration()
835+
{
836+
$container = new ContainerBuilder();
837+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c1 = static function (Definition $definition) {});
838+
$config = new ContainerBuilder();
839+
$config->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c2 = function (Definition $definition) {});
840+
841+
$container->merge($config);
842+
$this->assertSame([AsTaggedItem::class => [$c1, $c2]], $container->getAttributeAutoconfigurators());
843+
}
844+
845+
/**
846+
* @group legacy
847+
*/
848+
public function testGetAutoconfiguredAttributes()
849+
{
850+
$container = new ContainerBuilder();
851+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c = static function () {});
852+
853+
$this->expectUserDeprecationMessage('Since symfony/dependency-injection 7.3: The "Symfony\Component\DependencyInjection\ContainerBuilder::getAutoconfiguredAttributes()" method is deprecated, use "getAttributeAutoconfigurators()" instead.');
854+
$configurators = $container->getAutoconfiguredAttributes();
855+
$this->assertSame($c, $configurators[AsTaggedItem::class]);
856+
857+
// Method call fails with more than one configurator for a given attribute
858+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c = static function () {});
859+
860+
$this->expectException(LogicException::class);
861+
$container->getAutoconfiguredAttributes();
862+
}
863+
832864
public function testResolveEnvValues()
833865
{
834866
$_ENV['DUMMY_ENV_VAR'] = 'du%%y';

0 commit comments

Comments
 (0)
0