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

Skip to content

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 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()` 10BC0 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
}
@@ -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