From 138b1dd8bd021fa81bf1ecc9378f1cb27afce677 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Thu, 29 May 2025 23:22:17 -0300 Subject: [PATCH] [DependencyInjection] add `factory` argument in the `#[Autoconfigure]` attribute and allow instanceof factory --- .../Attribute/Autoconfigure.php | 2 + .../DependencyInjection/CHANGELOG.md | 1 + .../Configurator/InstanceofConfigurator.php | 1 + .../Loader/YamlFileLoader.php | 1 + .../schema/dic/services/services-1.0.xsd | 1 + ...egisterAutoconfigureAttributesPassTest.php | 88 +++++++++++++++++++ .../AutoconfigureWithExpressionFactory.php | 22 +++++ ...toconfigureWithInstanceExternalFactory.php | 22 +++++ .../AutoconfigureWithInvokableFactory.php | 22 +++++ ...AutoconfigureWithStaticExternalFactory.php | 22 +++++ .../AutoconfigureWithStaticSelfFactory.php | 27 ++++++ .../config/instanceof_factory.expected.yml | 16 ++++ .../Fixtures/config/instanceof_factory.php | 23 +++++ .../xml/services_instanceof_factory.xml | 16 ++++ .../yaml/service_instanceof_factory2.yml | 12 +++ .../Tests/Loader/PhpFileLoaderTest.php | 1 + .../Tests/Loader/XmlFileLoaderTest.php | 15 +++- .../Tests/Loader/YamlFileLoaderTest.php | 15 +++- 18 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithExpressionFactory.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInstanceExternalFactory.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInvokableFactory.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticExternalFactory.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticSelfFactory.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.expected.yml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_factory.xml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/service_instanceof_factory2.yml diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php index 06513fd903e01..4d7e592575070 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php @@ -30,6 +30,7 @@ class Autoconfigure * @param array|null $properties The properties to define when creating the service * @param array{string, string}|string|null $configurator A PHP function, reference or an array containing a class/reference and a method to call after the service is fully initialized * @param string|null $constructor The public static method to use to instantiate the service + * @param array|string|null $factory The factory that defines how to create the service */ public function __construct( public ?array $tags = null, @@ -42,6 +43,7 @@ public function __construct( public ?array $properties = null, public array|string|null $configurator = null, public ?string $constructor = null, + public array|string|null $factory = null, ) { } } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 5c6c41cfdf27b..ef3cc8d30f5db 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Allow `#[AsAlias]` to be extended + * Add support for a `factory` argument in the `#[Autoconfigure]` attribute to define service instantiation via a factory. Also enabled the use of `factory` within `instanceof` conditionals 7.3 --- diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php index a26e5a84becfb..e871ae353b304 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php @@ -23,6 +23,7 @@ class InstanceofConfigurator extends AbstractServiceConfigurator use Traits\CallTrait; use Traits\ConfiguratorTrait; use Traits\ConstructorTrait; + use Traits\FactoryTrait; use Traits\LazyTrait; use Traits\PropertyTrait; use Traits\PublicTrait; diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index c3b1bf255e8b1..748dc053dc7f4 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -100,6 +100,7 @@ class YamlFileLoader extends FileLoader 'autowire' => 'autowire', 'bind' => 'bind', 'constructor' => 'constructor', + 'factory' => 'factory', ]; private const DEFAULTS_KEYWORDS = [ 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 befdb658f38ef..aca2e86623f01 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 @@ -175,6 +175,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php index 931a4f5c4405b..aa8174ea011de 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php @@ -26,6 +26,12 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureRepeatedOverwrite; use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureRepeatedProperties; use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureRepeatedTag; +use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureWithExpressionFactory; +use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureWithInstanceExternalFactory; +use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureWithInvokableFactory; +use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureWithStaticExternalFactory; +use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureWithStaticSelfFactory; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FactoryDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\LazyAutoconfigured; use Symfony\Component\DependencyInjection\Tests\Fixtures\LazyLoaded; use Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleAutoconfigureAttributed; @@ -208,6 +214,88 @@ public function testStaticConstructor() $this->assertEquals([StaticConstructorAutoconfigure::class => $expected], $container->getAutoconfiguredInstanceof()); } + public function testAutoconfigureWithStaticSelfFactory() + { + $container = new ContainerBuilder(); + $container->register('foo', AutoconfigureWithStaticSelfFactory::class) + ->setAutoconfigured(true); + + $argument = new BoundArgument('foo', false, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureWithStaticSelfFactory.php')); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->setFactory([null, 'create']) + ->setBindings(['$foo' => $argument]) + ; + $this->assertEquals([AutoconfigureWithStaticSelfFactory::class => $expected], $container->getAutoconfiguredInstanceof()); + } + + public function testAutoconfigureWithStaticExternalFactory() + { + $container = new ContainerBuilder(); + $container->register('foo', AutoconfigureWithStaticExternalFactory::class) + ->setAutoconfigured(true); + + $argument = new BoundArgument('foo', false, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureWithStaticExternalFactory.php')); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->setFactory([FactoryDummy::class, 'create']) + ->setBindings(['$foo' => $argument]) + ; + $this->assertEquals([AutoconfigureWithStaticExternalFactory::class => $expected], $container->getAutoconfiguredInstanceof()); + } + + public function testAutoconfigureWithInstanceExternalFactory() + { + $container = new ContainerBuilder(); + $container->register('foo', AutoconfigureWithInstanceExternalFactory::class) + ->setAutoconfigured(true); + + $argument = new BoundArgument('foo', false, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureWithInstanceExternalFactory.php')); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->setFactory([new Reference('factory_for_autoconfigure'), 'createStatic']) + ->setBindings(['$foo' => $argument]) + ; + $this->assertEquals([AutoconfigureWithInstanceExternalFactory::class => $expected], $container->getAutoconfiguredInstanceof()); + } + + public function testAutoconfigureWithInvokableFactory() + { + $container = new ContainerBuilder(); + $container->register('foo', AutoconfigureWithInvokableFactory::class) + ->setAutoconfigured(true); + + $argument = new BoundArgument('foo', false, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureWithInvokableFactory.php')); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->setFactory([new Reference('factory_for_autoconfigure'), '__invoke']) + ->setBindings(['$foo' => $argument]) + ; + $this->assertEquals([AutoconfigureWithInvokableFactory::class => $expected], $container->getAutoconfiguredInstanceof()); + } + + public function testAutoconfigureWithExpressionFactory() + { + $container = new ContainerBuilder(); + $container->register('foo', AutoconfigureWithExpressionFactory::class) + ->setAutoconfigured(true); + + (new RegisterAutoconfigureAttributesPass())->process($container); + + $expected = (new ChildDefinition('')) + ->setFactory('@=service("factory_for_autoconfigure").create()') + ; + $this->assertEquals([AutoconfigureWithExpressionFactory::class => $expected], $container->getAutoconfiguredInstanceof()); + } + public function testLazyServiceAttribute() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithExpressionFactory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithExpressionFactory.php new file mode 100644 index 0000000000000..4e382ec2c63e0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithExpressionFactory.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + +#[Autoconfigure(factory: '@=service("factory_for_autoconfigure").create()')] +class AutoconfigureWithExpressionFactory +{ + public function __construct(public readonly string $foo) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInstanceExternalFactory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInstanceExternalFactory.php new file mode 100644 index 0000000000000..b6adc9dc598ef --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInstanceExternalFactory.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + +#[Autoconfigure(bind: ['$foo' => 'foo'], factory: ['@factory_for_autoconfigure', 'createStatic'])] +class AutoconfigureWithInstanceExternalFactory +{ + public function __construct(public readonly string $foo) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInvokableFactory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInvokableFactory.php new file mode 100644 index 0000000000000..9d482fddf2445 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithInvokableFactory.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + +#[Autoconfigure(bind: ['$foo' => 'foo'], factory: '@factory_for_autoconfigure')] +class AutoconfigureWithInvokableFactory +{ + public function __construct(public readonly string $foo) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticExternalFactory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticExternalFactory.php new file mode 100644 index 0000000000000..e05ef25adbfad --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticExternalFactory.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + +#[Autoconfigure(bind: ['$foo' => 'foo'], factory: [FactoryDummy::class, 'create'])] +class AutoconfigureWithStaticExternalFactory +{ + public function __construct(public readonly string $foo) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticSelfFactory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticSelfFactory.php new file mode 100644 index 0000000000000..b84fb9ebf1bb8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutoconfigureWithStaticSelfFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + +#[Autoconfigure(bind: ['$foo' => 'foo'], factory: [null, 'create'])] +class AutoconfigureWithStaticSelfFactory +{ + public function __construct(public readonly string $foo) + { + } + + public static function create(string $foo): static + { + return new self($foo); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.expected.yml new file mode 100644 index 0000000000000..1f7c75750c8a2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.expected.yml @@ -0,0 +1,16 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar + public: true + tags: + - bar + factory: ['@Symfony\Component\DependencyInjection\Tests\Fixtures\BarFactory', getDefaultBar] + Symfony\Component\DependencyInjection\Tests\Fixtures\BarFactory: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\BarFactory + public: true + arguments: [!tagged_iterator bar] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.php new file mode 100644 index 0000000000000..99c5b7c312927 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/instanceof_factory.php @@ -0,0 +1,23 @@ +services()->defaults()->public(); + + $services->instanceof(BarInterface::class) + ->factory([new Reference(BarFactory::class), 'getDefaultBar']) + ->tag('bar'); + + $services->set(Bar::class) + ->public(); + + $services->set(BarFactory::class) + ->args([new TaggedIteratorArgument('bar')]); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_factory.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_factory.xml new file mode 100644 index 0000000000000..c91081d7f6fad --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_factory.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/service_instanceof_factory2.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/service_instanceof_factory2.yml new file mode 100644 index 0000000000000..36417c0727ba3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/service_instanceof_factory2.yml @@ -0,0 +1,12 @@ +services: + _instanceof: + Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: + factory: ['@Symfony\Component\DependencyInjection\Tests\Fixtures\BarFactory', 'getDefaultBar'] + tags: + - { name: bar } + + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: + public: true + + Symfony\Component\DependencyInjection\Tests\Fixtures\BarFactory: + arguments: [!tagged_iterator 'bar'] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index 72ededfd07329..ffa57557749a5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -139,6 +139,7 @@ public static function provideConfig() yield ['static_constructor']; yield ['inline_static_constructor']; yield ['instanceof_static_constructor']; + yield ['instanceof_factory']; yield ['closure']; yield ['from_callable']; yield ['env_param']; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index f962fa1062bb5..7cd317a6ae4ea 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -941,11 +941,14 @@ public function testNamedArguments() $this->assertEquals([['setApiKey', ['123']]], $container->getDefinition(NamedArgumentsDummy::class)->getMethodCalls()); } - public function testInstanceof() + /** + * @dataProvider provideServiceInstanceOfFactoryFiles + */ + public function testInstanceof(string $fileName) { $container = new ContainerBuilder(); $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); - $loader->load('services_instanceof.xml'); + $loader->load($fileName); $container->compile(); $definition = $container->getDefinition(Bar::class); @@ -954,6 +957,14 @@ public function testInstanceof() $this->assertSame(['foo' => [[]], 'bar' => [[]]], $definition->getTags()); } + public static function provideServiceInstanceOfFactoryFiles(): iterable + { + return [ + ['services_instanceof.xml'], + ['services_instanceof_factory.xml'], + ]; + } + public function testEnumeration() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 54900e4c3e146..4119a1b1c5e22 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -990,17 +990,28 @@ public function testFqcnLazyProxy() $this->assertSame([['interface' => 'SomeInterface']], $definition->getTag('proxy')); } - public function testServiceWithSameNameAsInterfaceAndFactoryIsNotTagged() + /** + * @dataProvider provideServiceInstanceOfFactoryFiles + */ + public function testServiceWithSameNameAsInterfaceAndFactoryIsNotTagged(string $fileName) { $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('service_instanceof_factory.yml'); + $loader->load($fileName); $container->compile(); $tagged = $container->findTaggedServiceIds('bar'); $this->assertCount(1, $tagged); } + public static function provideServiceInstanceOfFactoryFiles(): iterable + { + return [ + ['service_instanceof_factory.yml'], + ['service_instanceof_factory2.yml'], + ]; + } + /** * The pass may throw an exception, which will cause the test to fail. */