Skip to content

Commit 7278aa0

Browse files
committed
feature symfony#61578 [DependencyInjection] Parse attributes found on abstract classes for resource definitions (nicolas-grekas)
This PR was merged into the 7.4 branch. Discussion ---------- [DependencyInjection] Parse attributes found on abstract classes for resource definitions | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT This PR improves support for resource definitions by allowing to parse attributes on abstract classes. This can be useful for PRs like symfony#61563 and symfony#61545, so that the attribute proposed there could be set on abstract classes. Commits ------- 6571f7b [DependencyInjection] Parse attributes found on abstract classes for resource definitions
2 parents 83c2aec + 6571f7b commit 7278aa0

File tree

16 files changed

+115
-33
lines changed

16 files changed

+115
-33
lines changed

UPGRADE-7.4.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ DependencyInjection
2727
-------------------
2828

2929
* Add argument `$target` to `ContainerBuilder::registerAliasForArgument()`
30+
* Add argument `$throwOnAbstract` to `ContainerBuilder::findTaggedResourceIds()`
3031
* Deprecate registering a service without a class when its id is a non-existing FQCN
3132

3233
DoctrineBridge

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Allow adding resource tags using any config format
88
* Allow `#[AsAlias]` to be extended
9+
* Parse attributes found on abstract classes for resource definitions
910
* Add argument `$target` to `ContainerBuilder::registerAliasForArgument()`
1011
* Deprecate registering a service without a class when its id is a non-existing FQCN
1112

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,18 @@ public function process(ContainerBuilder $container): void
7979
}
8080
}
8181

82-
parent::process($container);
82+
$this->container = $container;
83+
foreach ($container->getDefinitions() as $id => $definition) {
84+
$this->currentId = $id;
85+
$this->processValue($definition, true);
86+
}
8387
}
8488

8589
protected function processValue(mixed $value, bool $isRoot = false): mixed
8690
{
8791
if (!$value instanceof Definition
8892
|| !$value->isAutoconfigured()
89-
|| $value->isAbstract()
93+
|| ($value->isAbstract() && !$value->hasTag('container.excluded'))
9094
|| $value->hasTag('container.ignore_attributes')
9195
|| !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
9296
) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private function processValue(mixed $value, int $rootLevel = 0, int $level = 0):
6262
} elseif ($value instanceof ArgumentInterface) {
6363
$value->setValues($this->processValue($value->getValues(), $rootLevel, 1 + $level));
6464
} elseif ($value instanceof Definition) {
65-
if ($value->isSynthetic() || $value->isAbstract()) {
65+
if ($value->isSynthetic() || $value->isAbstract() || $value->hasTag('container.excluded')) {
6666
return $value;
6767
}
6868
$value->setArguments($this->processValue($value->getArguments(), 0));

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,19 +1366,30 @@ public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false
13661366
* }
13671367
* }
13681368
*
1369+
* @param bool $throwOnAbstract
1370+
*
13691371
* @return array<string, array> An array of tags with the tagged service as key, holding a list of attribute arrays
13701372
*/
1371-
public function findTaggedResourceIds(string $tagName): array
1373+
public function findTaggedResourceIds(string $tagName/* , bool $throwOnAbstract = true */): array
13721374
{
1375+
$throwOnAbstract = \func_num_args() > 1 ? func_get_arg(1) : true;
13731376
$this->usedTags[] = $tagName;
13741377
$tags = [];
13751378
foreach ($this->getDefinitions() as $id => $definition) {
1376-
if ($definition->hasTag($tagName)) {
1377-
if (!$definition->hasTag('container.excluded')) {
1378-
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName));
1379-
}
1380-
$tags[$id] = $definition->getTag($tagName);
1379+
if (!$definition->hasTag($tagName)) {
1380+
continue;
1381+
}
1382+
if (!$definition->hasTag('container.excluded')) {
1383+
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName));
1384+
}
1385+
$class = $this->parameterBag->resolveValue($definition->getClass());
1386+
if (!$class || $throwOnAbstract && $definition->isAbstract()) {
1387+
throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" must have a class and not be abstract.', $id, $tagName));
1388+
}
1389+
if ($definition->getClass() !== $class) {
1390+
$definition->setClass($class);
13811391
}
1392+
$tags[$id] = $definition->getTag($tagName);
13821393
}
13831394

13841395
return $tags;

src/Symfony/Component/DependencyInjection/Definition.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,7 @@ public function addTag(string $name, array $attributes = []): static
465465
public function addResourceTag(string $name, array $attributes = []): static
466466
{
467467
return $this->addTag($name, $attributes)
468-
->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)])
469-
->setAbstract(true);
468+
->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)]);
470469
}
471470

472471
/**

src/Symfony/Component/DependencyInjection/Loader/FileLoader.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,22 +189,26 @@ public function registerClasses(Definition $prototype, string $namespace, string
189189
}
190190

191191
$r = null === $errorMessage ? $this->container->getReflectionClass($class) : null;
192-
if ($r?->isAbstract() || $r?->isInterface()) {
193-
if ($r->isInterface()) {
194-
$this->interfaces[] = $class;
195-
}
196-
$autoconfigureAttributes?->processClass($this->container, $r);
197-
continue;
198-
}
199192

200-
$this->setDefinition($class, $definition = $getPrototype());
193+
$abstract = $r?->isAbstract() || $r?->isInterface() ? '.abstract.' : '';
194+
$this->setDefinition($abstract.$class, $definition = $getPrototype());
201195
if (null !== $errorMessage) {
202196
$definition->addError($errorMessage);
203197

204198
continue;
205199
}
206200
$definition->setClass($class);
207201

202+
if ($abstract) {
203+
if ($r->isInterface()) {
204+
$this->interfaces[] = $class;
205+
}
206+
$autoconfigureAttributes?->processClass($this->container, $r);
207+
$definition->setAbstract(true)
208+
->addTag('container.excluded', ['source' => 'because the class is abstract']);
209+
continue;
210+
}
211+
208212
$interfaces = [];
209213
foreach (class_implements($class, false) as $interface) {
210214
$this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class;

src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,31 @@ public function testAttributeConfiguratorCallableMissingType()
4545
$this->expectExceptionMessage('Argument "$reflector" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in ');
4646
(new AttributeAutoconfigurationPass())->process($container);
4747
}
48+
49+
public function testProcessesAbstractServicesWithContainerExcludedTag()
50+
{
51+
$container = new ContainerBuilder();
52+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, static function (ChildDefinition $definition, AsTaggedItem $attribute, \ReflectionClass $reflector) {
53+
$definition->addTag('processed.tag');
54+
});
55+
56+
// Create an abstract service with container.excluded tag and attributes
57+
$abstractService = $container->register('abstract_service', TestServiceWithAttributes::class)
58+
->setAutoconfigured(true)
59+
->setAbstract(true)
60+
->addTag('container.excluded', ['source' => 'test']);
61+
62+
(new AttributeAutoconfigurationPass())->process($container);
63+
64+
// Abstract service with container.excluded tag should be processed
65+
$expected = [
66+
TestServiceWithAttributes::class => (new ChildDefinition(''))->addTag('processed.tag'),
67+
];
68+
$this->assertEquals($expected, $abstractService->getInstanceofConditionals());
69+
}
70+
}
71+
72+
#[AsTaggedItem]
73+
class TestServiceWithAttributes
74+
{
4875
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public function testAddResourceTag()
262262
$def->addResourceTag('foo', ['bar' => true]);
263263

264264
$this->assertSame([['bar' => true]], $def->getTag('foo'));
265-
$this->assertTrue($def->isAbstract());
265+
$this->assertFalse($def->isAbstract());
266266
$this->assertSame([['source' => 'by tag "foo"']], $def->getTag('container.excluded'));
267267
}
268268

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
4+
5+
abstract class AbstractClass
6+
{
7+
}

0 commit comments

Comments
 (0)