8000 [DependencyInjection] Enable multiple attribute autoconfiguration cal… · symfony/symfony@71d0ce7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 71d0ce7

Browse files
committed
[DependencyInjection] Enable multiple attribute autoconfiguration callables on the same class
1 parent 07e020a commit 71d0ce7

File tree

5 files changed

+111
-50
lines changed

5 files changed

+111
-50
lines changed

UPGRADE-7.3.md

Lines changed: 5 additions & 0 deletions
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
4141

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

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Add `Definition::addResourceTag()` and `ContainerBuilder::findTaggedResourceIds()`
1111
for auto-configuration of classes excluded from the service container
1212
* Leverage native lazy objects when possible for lazy services
13+
* Accept multiple attribute autoconfiguration callbacks for the same class
1314

1415
7.2
1516
---

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

Lines changed: 43 additions & 41 deletions
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->getAttributeConfigurators()) {
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->getAttributeConfigurators() 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

@@ -96,7 +98,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
9698

9799
if ($this->classAttributeConfigurators) {
98100
foreach ($classReflector->getAttributes() as $attribute) {
99-
if ($configurator = $this->findConfigurator($this->classAttributeConfigurators, $attribute->getName())) {
101+
foreach($this->findConfigurators($this->classAttributeConfigurators, $attribute->getName()) as $configurator) {
100102
$configurator($conditionals, $attribute->newInstance(), $classReflector);
101103
}
102104
}
@@ -112,7 +114,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
112114
if ($constructorReflector) {
113115
foreach ($constructorReflector->getParameters() as $parameterReflector) {
114116
foreach ($parameterReflector->getAttributes() as $attribute) {
115-
if ($configurator = $this->findConfigurator($this->parameterAttributeConfigurators, $attribute->getName())) {
117+
foreach($this->findConfigurators($this->parameterAttributeConfigurators, $attribute->getName()) as $configurator) {
116118
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
117119
}
118120
}
@@ -128,7 +130,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
128130

129131
if ($this->methodAttributeConfigurators) {
130132
foreach ($methodReflector->getAttributes() as $attribute) {
131-
if ($configurator = $this->findConfigurator($this->methodAttributeConfigurators, $attribute->getName())) {
133+
foreach($this->findConfigurators($this->methodAttributeConfigurators, $attribute->getName()) as $configurator) {
132134
$configurator($conditionals, $attribute->newInstance(), $methodReflector);
133135
}
134136
F438 }
@@ -137,7 +139,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
137139
if ($this->parameterAttributeConfigurators) {
138140
foreach ($methodReflector->getParameters() as $parameterReflector) {
139141
foreach ($parameterReflector->getAttributes() as $attribute) {
140-
if ($configurator = $this->findConfigurator($this->parameterAttributeConfigurators, $attribute->getName())) {
142+
foreach($this->findConfigurators($this->parameterAttributeConfigurators, $attribute->getName()) as $configurator) {
141143
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
142144
}
143145
}
@@ -153,7 +155,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
153155
}
154156

155157
foreach ($propertyReflector->getAttributes() as $attribute) {
156-
if ($configurator = $this->findConfigurator($this->propertyAttributeConfigurators, $attribute->getName())) {
158+
foreach($this->findConfigurators($this->propertyAttributeConfigurators, $attribute->getName()) as $configurator) {
157159
$configurator($conditionals, $attribute->newInstance(), $propertyReflector);
158160
}
159161
}
@@ -171,16 +173,16 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
171173
/**
172174
* Find the first configurator for the given attribute name, looking up the class hierarchy.
173175
*/
174-
private function findConfigurator(array &$configurators, string $attributeName): ?callable
176+
private function findConfigurators(array &$configurators, string $attributeName): array
175177
{
176178
if (\array_key_exists($attributeName, $configurators)) {
177179
return $configurators[$attributeName];
178180
}
179181

180182
if (class_exists($attributeName) && $parent = get_parent_class($attributeName)) {
181-
return $configurators[$attributeName] = self::findConfigurator($configurators, $parent);
183+
return $configurators[$attributeName] = self::findConfigurators($configurators, $parent);
182184
}
183185

184-
return $configurators[$attributeName] = null;
186+
return $configurators[$attributeName] = [];
185187
}
186188
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 27 additions & 9 deletions
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,8 @@ 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->getAttributeConfigurators() as $attribute => $configurators) {
721+
$this->autoconfiguredAttributes[$attribute] = [...$this->autoconfiguredAttributes[$attribute], ...$configurators];
726722
}
727723
}
728724

@@ -1448,7 +1444,7 @@ public function registerForAutoconfiguration(string $interface): ChildDefinition
14481444
*/
14491445
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
14501446
{
1451-
$this->autoconfiguredAttributes[$attributeClass] = $configurator;
1447+
$this->autoconfiguredAttributes[$attributeClass][] = $configurator;
14521448
}
14531449

14541450
/**
@@ -1489,9 +1485,31 @@ public function getAutoconfiguredInstanceof(): array
14891485
}
14901486

14911487
/**
1492-
* @return array<string, callable>
1488+
* @return array<class-string, callable>
1489+
*
1490+
* @deprecated Use {@see getAttributeConfigurators()} instead
14931491
*/
14941492
public function getAutoconfiguredAttributes(): array
1493+
{
1494+
trigger_deprecation('symfony/dependency-injection', '7.3', 'The "%s()" method is deprecated, use "getAttributeConfigurators()" instead.', __METHOD__);
1495+
1496+
return array_map(static function (array $configurators): callable {
1497+
if (count($configurators) === 1) {
1498+
return $configurators[0];
1499+
}
1500+
1501+
return static function (...$args) use ($configurators) {
1502+
foreach ($configurators as $configurator) {
1503+
$configurator(...$args);
1504+
}
1505+
};
1506+
}, $this->autoconfiguredAttributes);
1507+
}
1508+
1509+
/**
1510+
* @return array<class-string, callable[]>
1511+
*/
1512+
public function getAttributeConfigurators(): array
14951513
{
14961514
return $this->autoconfiguredAttributes;
14971515
}

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

Lines changed: 35 additions & 0 deletions
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;
@@ -829,6 +830,40 @@ public function testMergeThrowsExceptionForDuplicateAutomaticInstanceofDefinitio
829830
$container->merge($config);
830831
}
831832

833+
public function testMergeAttributeAutoconfiguration()
834+
{
835+
$container = new ContainerBuilder();
836+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c1 = static function (Definition $definition) {});
837+
$config = new ContainerBuilder();
838+
$config->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c2 = function (Definition $definition) {});
839+
840+
$container->merge($config);
841+
$this->assertSame([AsTaggedItem::class => [$c1, $c2]], $container->getAttributeConfigurators());
842+
}
843+
844+
/**
845+
* @group legacy
846+
*/
847+
public function testLegacyAutoconfiguredAttributes()
848+
{
849+
$container = new ContainerBuilder();
850+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, function (Definition $definition) {
851+
$definition->addTag('tagged_item', ['v' => 1]);
852+
});
853+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, function (Definition $definition) {
854+
$definition->addTag('tagged_item', ['v' => 2]);
855+
});
856+
857+
$this->expectUserDeprecationMessage('Since symfony/dependency-injection 7.3: The "Symfony\Component\DependencyInjection\ContainerBuilder::getAutoconfiguredAttributes()" method is deprecated, use "getAttributeConfigurators()" instead.');
858+
859+
$configurators = $container->getAutoconfiguredAttributes();
860+
$this->assertIsCallable($configurators[AsTaggedItem::class]);
861+
862+
// All configurators are called
863+
$configurators[AsTaggedItem::class]($definition = new ChildDefinition('foo'));
864+
$this->assertSame([['v' => 1], ['v' => 2]], $definition->getTag('tagged_item'));
865+
}
866+
832867
public function testResolveEnvValues()
833868
{
834869
$_ENV['DUMMY_ENV_VAR'] = 'du%%y';

0 commit comments

Comments
 (0)
0