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
*/