diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Service.php b/src/Symfony/Component/DependencyInjection/Attribute/Service.php new file mode 100644 index 0000000000000..745aaf488ab51 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/Service.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +/** + * An attribute to explicitly mark a class as a service definition. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class Service +{ +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 54095a8d37ae5..2b5e126b4e45a 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + +* Explicitly add service definitions with a new `Service` attribute + 7.1 --- diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php index da8ac0a44c1f1..5ed865cd832ba 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php @@ -40,6 +40,8 @@ class PrototypeConfigurator extends AbstractServiceConfigurator private ?array $excludes = null; + private bool $onlyWithServiceAttribute = false; + public function __construct( ServicesConfigurator $parent, private PhpFileLoader $loader, @@ -67,11 +69,18 @@ public function __destruct() parent::__destruct(); if (isset($this->loader)) { - $this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->excludes, $this->path); + $this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->excludes, $this->path, $this->onlyWithServiceAttribute); } unset($this->loader); } + public function onlyWithServiceAttribute(bool $onlyWithServiceAttribute = true): static + { + $this->onlyWithServiceAttribute = $onlyWithServiceAttribute; + + return $this; + } + /** * Excludes files from registration using glob patterns. * diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 22bd4c2243762..1bf513bd7fc1d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\DependencyInjection\Attribute\Exclude; +use Symfony\Component\DependencyInjection\Attribute\Service; use Symfony\Component\DependencyInjection\Attribute\When; use Symfony\Component\DependencyInjection\Attribute\WhenNot; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -100,13 +101,14 @@ public function import(mixed $resource, ?string $type = null, bool|string $ignor /** * Registers a set of classes as services using PSR-4 for discovery. * - * @param Definition $prototype A definition to use as template - * @param string $namespace The namespace prefix of classes in the scanned directory - * @param string $resource The directory to look for classes, glob-patterns allowed - * @param string|string[]|null $exclude A globbed path of files to exclude or an array of globbed paths of files to exclude - * @param string|null $source The path to the file that defines the auto-discovery rule + * @param Definition $prototype A definition to use as template + * @param string $namespace The namespace prefix of classes in the scanned directory + * @param string $resource The directory to look for classes, glob-patterns allowed + * @param string|string[]|null $exclude A globbed path of files to exclude or an array of globbed paths of files to exclude + * @param string|null $source The path to the file that defines the auto-discovery rule + * @param bool|null $onlyWithServiceAttribute Whether to include only classes with the service attribute */ - public function registerClasses(Definition $prototype, string $namespace, string $resource, string|array|null $exclude = null, ?string $source = null): void + public function registerClasses(Definition $prototype, string $namespace, string $resource, string|array|null $exclude = null, ?string $source = null, ?bool $onlyWithServiceAttribute = null): void { if (!str_ends_with($namespace, '\\')) { throw new InvalidArgumentException(\sprintf('Namespace prefix must end with a "\\": "%s".', $namespace)); @@ -154,13 +156,18 @@ public function registerClasses(Definition $prototype, string $namespace, string $this->addContainerExcludedTag($class, $source); continue; } + if ($onlyWithServiceAttribute && !($r->getAttributes(Service::class)[0] ?? null)) { + $this->addContainerExcludedTag($class, $source); + continue; + } + if ($this->env) { $excluded = true; $whenAttributes = $r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF); $notWhenAttributes = $r->getAttributes(WhenNot::class, \ReflectionAttribute::IS_INSTANCEOF); if ($whenAttributes && $notWhenAttributes) { - throw new LogicException(sprintf('The "%s" class cannot have both #[When] and #[WhenNot] attributes.', $class)); + throw new LogicException(\sprintf('The "%s" class cannot have both #[When] and #[WhenNot] attributes.', $class)); } if (!$whenAttributes && !$notWhenAttributes) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 9283598a571f4..5de1f6efc5820 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -179,7 +179,8 @@ private function parseDefinitions(\DOMDocument $xml, string $file, Definition $d } $excludes = [$service->getAttribute('exclude')]; } - $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes, $file); + $onlyWithServiceAttribute = XmlUtils::phpize($service->getAttribute('onlyWithServiceAttribute')); + $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes, $file, $onlyWithServiceAttribute); } else { $this->setDefinition((string) $service->getAttribute('id'), $definition); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index afb0b162666ac..9c1c91156d219 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -87,6 +87,7 @@ class YamlFileLoader extends FileLoader 'autoconfigure' => 'autoconfigure', 'bind' => 'bind', 'constructor' => 'constructor', + 'onlyWithServiceAttribute' => 'onlyWithServiceAttribute', ]; private const INSTANCEOF_KEYWORDS = [ @@ -707,7 +708,8 @@ private function parseDefinition(string $id, array|string|null $service, string } $exclude = $service['exclude'] ?? null; $namespace = $service['namespace'] ?? $id; - $this->registerClasses($definition, $namespace, $service['resource'], $exclude, $file); + $onlyWithServiceAttribute = $service['onlyWithServiceAttribute'] ?? null; + $this->registerClasses($definition, $namespace, $service['resource'], $exclude, $file, $onlyWithServiceAttribute); } else { $this->setDefinition($id, $definition); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index c071e3466613c..fe1f5b1ea8452 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -212,6 +212,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/ServiceAttributes/BarWithService.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/ServiceAttributes/BarWithService.php new file mode 100644 index 0000000000000..5d4be94a87f14 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/ServiceAttributes/BarWithService.php @@ -0,0 +1,17 @@ +load(Prototype::class.'\\', '../Prototype') ->public() ->autoconfigure() - ->exclude('../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}') + ->exclude('../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor,ServiceAttributes}') ->factory('f') ->deprecate('vendor/package', '1.1', '%service_id%') ->args([0]) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php index bb40e08ec4328..59f6764ec7f97 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php @@ -10,7 +10,7 @@ $di->load(Prototype::class.'\\', '../Prototype') ->public() ->autoconfigure() - ->exclude(['../Prototype/OtherDir', '../Prototype/BadClasses', '../Prototype/BadAttributes', '../Prototype/SinglyImplementedInterface', '../Prototype/StaticConstructor']) + ->exclude(['../Prototype/OtherDir', '../Prototype/BadClasses', '../Prototype/BadAttributes', '../Prototype/SinglyImplementedInterface', '../Prototype/StaticConstructor', '../Prototype/ServiceAttributes']) ->factory('f') ->deprecate('vendor/package', '1.1', '%service_id%') ->args([0]) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_service_attributes.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_service_attributes.expected.yml new file mode 100644 index 0000000000000..c09408ec853d0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_service_attributes.expected.yml @@ -0,0 +1,34 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService + public: true + tags: + - foo + - baz + deprecated: + package: vendor/package + version: '1.1' + message: '%service_id%' + autoconfigure: true + lazy: true + arguments: [1] + factory: f + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService + public: true + tags: + - foo + - baz + deprecated: + package: vendor/package + version: '1.1' + message: '%service_id%' + autoconfigure: true + lazy: true + arguments: [1] + factory: f diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_service_attributes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_service_attributes.php new file mode 100644 index 0000000000000..e8abbdc9088cf --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_service_attributes.php @@ -0,0 +1,24 @@ +services()->defaults() + ->tag('baz'); + + $di->load(Prototype::class.'\\', '../Prototype') + ->onlyWithServiceAttribute() + ->public() + ->autoconfigure() + ->exclude('../Prototype/{BadClasses,BadAttributes}') + ->factory('f') + ->deprecate('vendor/package', '1.1', '%service_id%') + ->args([0]) + ->args([1]) + ->tag('foo') + ->parent('foo'); + + $di->set('foo')->lazy()->abstract()->public(); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml index 4dec14b32317b..05df24ad75d27 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml @@ -1,6 +1,6 @@ - + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml index 2780d582bc788..cb26dd083bf0d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml @@ -7,6 +7,7 @@ ../Prototype/BadAttributes ../Prototype/SinglyImplementedInterface ../Prototype/StaticConstructor + ../Prototype/ServiceAttributes diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml index 7a8c9c544bfb9..7ef8bbbdb7876 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array_with_space_node.xml @@ -7,6 +7,7 @@ ../Prototype/BadAttributes ../Prototype/SinglyImplementedInterface ../Prototype/StaticConstructor + ../Prototype/ServiceAttributes diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_only_with_service_attribute.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_only_with_service_attribute.xml new file mode 100644 index 0000000000000..1c6e308f1f99e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_only_with_service_attribute.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml index 7eff75294b179..5331da2cc4a60 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml @@ -1,4 +1,4 @@ services: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: resource: ../Prototype - exclude: '../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}' + exclude: '../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor,ServiceAttributes}' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype_only_with_service_attribute.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype_only_with_service_attribute.yml new file mode 100644 index 0000000000000..7e6b79d14fbc2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype_only_with_service_attribute.yml @@ -0,0 +1,6 @@ +services: + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: + resource: ../Prototype + exclude: '../Prototype/{BadClasses,BadAttributes}' + onlyWithServiceAttribute: true + autoconfigure: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php index a195f2a93410c..221cf1406ef59 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -33,6 +33,8 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\BarInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasBarInterface; @@ -411,6 +413,33 @@ public function testRegisterClassesWithAsAliasAndImplementingMultipleInterfaces( 'PrototypeAsAlias/{WithAsAliasMultipleInterface,AliasBarInterface,AliasFooInterface}.php' ); } + + public function testRegisterWithOnlyServiceAttribute() + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $loader->registerClasses( + (new Definition())->setAutoconfigured(true), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', + 'Prototype/*', + 'Prototype/{BadAttributes,BadClasses}', + null, + true + ); + + $ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded'))); + sort($ids); + + $this->assertSame([ + BarWithService::class, + FooWithService::class, + 'service_container', + ], $ids); + + $this->assertTrue($container->has(FooWithService::class)); + $this->assertTrue($container->has(BarWithService::class)); + } } class TestFileLoader extends FileLoader diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 64df6cc7f79b5..5339bbd736745 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -798,6 +798,7 @@ public function testPrototype() str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadAttributes') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'SinglyImplementedInterface') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'StaticConstructor') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'ServiceAttributes') => true, ] ); $this->assertContains((string) $globResource, $resources); @@ -835,6 +836,7 @@ public function testPrototypeExcludeWithArray(string $fileName) str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'OtherDir') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'SinglyImplementedInterface') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'StaticConstructor') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'ServiceAttributes') => true, ] ); $this->assertContains((string) $globResource, $resources); @@ -861,6 +863,42 @@ public function testPrototypeExcludeWithArrayWithEmptyNode() $loader->load('services_prototype_array_with_empty_node.xml'); } + public function testPrototypeIncludesOnlyClassesServiceAttributes() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_prototype_only_with_service_attribute.xml'); + + $ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded'))); + sort($ids); + + $this->assertSame([ + Prototype\ServiceAttributes\BarWithService::class, + Prototype\ServiceAttributes\FooWithService::class, + 'service_container', + ], $ids); + + $resources = array_map('strval', $container->getResources()); + + $fixturesDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR; + $this->assertContains((string) new FileResource($fixturesDir.'xml'.\DIRECTORY_SEPARATOR.'services_prototype_only_with_service_attribute.xml'), $resources); + + $prototypeRealPath = realpath(__DIR__.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'Prototype'); + $globResource = new GlobResource( + $fixturesDir.'Prototype', + '/*', + true, + false, + [ + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadClasses') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadAttributes') => true, + ] + ); + $this->assertContains((string) $globResource, $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService', $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService', $resources); + } + public function testAliasDefinitionContainsUnsupportedElements() { $this->expectException(InvalidArgumentException::class); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 6aa4376525893..7787efdb95ddd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -569,6 +569,7 @@ public function testPrototype() str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'OtherDir') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'SinglyImplementedInterface') => true, str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'StaticConstructor') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'ServiceAttributes') => true, ] ); $this->assertContains((string) $globResource, $resources); @@ -576,6 +577,42 @@ public function testPrototype() $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources); } + public function testPrototypeIncludesOnlyClassesServiceAttributes() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_prototype_only_with_service_attribute.yml'); + + $ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded'))); + sort($ids); + + $this->assertSame([ + Prototype\ServiceAttributes\BarWithService::class, + Prototype\ServiceAttributes\FooWithService::class, + 'service_container', + ], $ids); + + $resources = array_map('strval', $container->getResources()); + + $fixturesDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR; + $this->assertContains((string) new FileResource($fixturesDir.'yaml'.\DIRECTORY_SEPARATOR.'services_prototype_only_with_service_attribute.yml'), $resources); + + $prototypeRealPath = realpath(__DIR__.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'Prototype'); + $globResource = new GlobResource( + $fixturesDir.'Prototype', + '', + true, + false, [ + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadClasses') => true, + str_replace(\DIRECTORY_SEPARATOR, '/', $prototypeRealPath.\DIRECTORY_SEPARATOR.'BadAttributes') => true, + ] + ); + + $this->assertContains((string) $globResource, $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService', $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService', $resources); + } + /** * @dataProvider prototypeWithNullOrEmptyNodeDataProvider */