From 7a0443b5005506cf003a90991477ff8129abf263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 5 Feb 2025 14:11:25 +0100 Subject: [PATCH] [DependencyInjection] Add `Definition::addExcludedTag()` and `ContainerBuilder::findExcludedServiceIds()` for auto-discovering value-objects --- .../DependencyInjection/CHANGELOG.md | 2 + .../DependencyInjection/ContainerBuilder.php | 32 +++++++++++ .../DependencyInjection/Definition.php | 14 +++++ .../Tests/ContainerBuilderTest.php | 55 ++++++++++++++++--- .../Tests/DefinitionTest.php | 10 ++++ 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index d24bb13c9bea3..0646d04bc39b6 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG * Make `#[AsTaggedItem]` repeatable * Support `@>` as a shorthand for `!service_closure` in yaml files * Don't skip classes with private constructor when autodiscovering + * Add `Definition::addExcludeTag()` and `ContainerBuilder::findExcludedServiceIds()` + for auto-configuration of classes excluded from the service container 7.2 --- diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 7389ca6310447..f5270e31ae076 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1351,6 +1351,38 @@ public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false return $tags; } + /** + * Returns service ids for a given tag, asserting they have the "container.excluded" tag. + * + * Example: + * + * $container->register('foo')->addExcludeTag('my.tag', ['hello' => 'world']) + * + * $serviceIds = $container->findExcludedServiceIds('my.tag'); + * foreach ($serviceIds as $serviceId => $tags) { + * foreach ($tags as $tag) { + * echo $tag['hello']; + * } + * } + * + * @return array An array of tags with the tagged service as key, holding a list of attribute arrays + */ + public function findExcludedServiceIds(string $tagName): array + { + $this->usedTags[] = $tagName; + $tags = []; + foreach ($this->getDefinitions() as $id => $definition) { + if ($definition->hasTag($tagName)) { + if (!$definition->hasTag('container.excluded')) { + throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName)); + } + $tags[$id] = $definition->getTag($tagName); + } + } + + return $tags; + } + /** * Returns all tags the defined services use. * diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 0abdc5d560cda..682540e91a289 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -455,6 +455,20 @@ public function addTag(string $name, array $attributes = []): static return $this; } + /** + * Adds a tag to the definition and marks it as excluded. + * + * These definitions should be processed using {@see ContainerBuilder::findExcludedServiceIds()} + * + * @return $this + */ + public function addExcludeTag(string $name, array $attributes = []): static + { + return $this->addTag($name, $attributes) + ->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)]) + ->setAbstract(true); + } + /** * Whether this definition has a tag with the given name. */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 544303bbe859a..882fffd19bda6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1062,20 +1062,18 @@ public function testMergeLogicException() $container->merge(new ContainerBuilder()); } - public function testfindTaggedServiceIds() + public function testFindTaggedServiceIds() { $builder = new ContainerBuilder(); - $builder - ->register('foo', 'Bar\FooClass') + $builder->register('foo', 'Bar\FooClass') + ->setAbstract(true) ->addTag('foo', ['foo' => 'foo']) ->addTag('bar', ['bar' => 'bar']) - ->addTag('foo', ['foofoo' => 'foofoo']) - ; - $builder - ->register('bar', 'Bar\FooClass') + ->addTag('foo', ['foofoo' => 'foofoo']); + $builder->register('bar', 'Bar\FooClass') ->addTag('foo') - ->addTag('container.excluded') - ; + ->addTag('container.excluded'); + $this->assertEquals([ 'foo' => [ ['foo' => 'foo'], @@ -1085,6 +1083,45 @@ public function testfindTaggedServiceIds() $this->assertEquals([], $builder->findTaggedServiceIds('foobar'), '->findTaggedServiceIds() returns an empty array if there is annotated services'); } + public function testFindTaggedServiceIdsThrowsWhenAbstract() + { + $builder = new ContainerBuilder(); + $builder->register('foo', 'Bar\FooClass') + ->setAbstract(true) + ->addTag('foo', ['foo' => 'foo']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The service "foo" tagged "foo" must not be abstract.'); + $builder->findTaggedServiceIds('foo', true); + } + + public function testFindExcludedServiceIds() + { + $builder = new ContainerBuilder(); + $builder->register('myservice', 'Bar\FooClass') + ->addTag('foo', ['foo' => 'foo']) + ->addTag('bar', ['bar' => 'bar']) + ->addTag('foo', ['foofoo' => 'foofoo']) + ->addExcludeTag('container.excluded'); + + $expected = ['myservice' => [['foo' => 'foo'], ['foofoo' => 'foofoo']]]; + $this->assertSame($expected, $builder->findExcludedServiceIds('foo')); + $this->assertSame([], $builder->findExcludedServiceIds('foofoo')); + } + + public function testFindExcludedServiceIdsThrowsWhenNotExcluded() + { + $builder = new ContainerBuilder(); + $builder->register('myservice', 'Bar\FooClass') + ->addTag('foo', ['foo' => 'foo']) + ->addTag('bar', ['bar' => 'bar']) + ->addTag('foo', ['foofoo' => 'foofoo']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The service "myservice" tagged "foo" is missing the "container.excluded" tag.'); + $builder->findExcludedServiceIds('foo', true); + } + public function testFindUnusedTags() { $builder = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index 3a7c3a98002ca..1a51c9af395c1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -258,6 +258,16 @@ public function testTags() ], $def->getTags(), '->getTags() returns all tags'); } + public function testAddExcludeTag() + { + $def = new Definition('stdClass'); + $def->addExcludeTag('foo', ['bar' => true]); + + $this->assertSame([['bar' => true]], $def->getTag('foo')); + $this->assertTrue($def->isAbstract()); + $this->assertSame([['source' => 'by tag "foo"']], $def->getTag('container.excluded')); + } + public function testSetArgument() { $def = new Definition('stdClass');