Skip to content

Navigation Menu

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 caa6d0e

Browse filesBrowse 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
Copy full SHA for caa6d0e

File tree

5 files changed

+129
-77
lines changed
Filter options

5 files changed

+129
-77
lines changed

‎UPGRADE-7.3.md

Copy file name to clipboardExpand all lines: 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
4141

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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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

Copy file name to clipboardExpand all lines: 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.