Skip to content

Commit c5fd454

Browse files
committed
feature #30255 [DependencyInjection] Invokable Factory Services (zanbaldwin)
This PR was squashed before being merged into the 4.3-dev branch (closes #30255). Discussion ---------- [DependencyInjection] Invokable Factory Services | Q | A | ------------- | --- | Branch? | `master` | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | symfony/symfony-docs#11014 > Failing test is in the Twig bridge, and outside of the the scope of this PR. Allow referencing invokable factory services, just as route definitions reference invokable controllers. This functionality was also added for service configurators for consistency. ## Example ```php <?php namespace App\Factory; class ServiceFactory { public function __invoke(bool $debug) { return new Service($debug); } } ``` ```yaml services: App\Service: # Prepend with "@" to differentiate between service and function. factory: '@app\Factory\ServiceFactory' arguments: [ '%kernel.debug%' ] ``` ```xml <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <!-- ... --> <service id="App\Service" class="App\Service"> <factory service="App\Factory\ServiceFactory" /> </service> </services> </container> ``` ```php <?php use App\Service; use App\Factory\ServiceFactory; use Symfony\Component\DependencyInjection\Reference; $container->register(Service::class, Service::class) ->setFactory(new Reference(ServiceFactory::class)); ``` Commits ------- 23cb83f726 [DependencyInjection] Invokable Factory Services
2 parents 22d0ce5 + c42b132 commit c5fd454

File tree

10 files changed

+40
-5
lines changed

10 files changed

+40
-5
lines changed

Definition.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public function setChanges(array $changes)
9595
/**
9696
* Sets a factory.
9797
*
98-
* @param string|array $factory A PHP function or an array containing a class/Reference and a method to call
98+
* @param string|array|Reference $factory A PHP function, reference or an array containing a class/Reference and a method to call
9999
*
100100
* @return $this
101101
*/
@@ -105,6 +105,8 @@ public function setFactory($factory)
105105

106106
if (\is_string($factory) && false !== strpos($factory, '::')) {
107107
$factory = explode('::', $factory, 2);
108+
} elseif ($factory instanceof Reference) {
109+
$factory = [$factory, '__invoke'];
108110
}
109111

110112
$this->factory = $factory;
@@ -783,7 +785,7 @@ public function getDeprecationMessage($id)
783785
/**
784786
* Sets a configurator to call after the service is fully initialized.
785787
*
786-
* @param string|array $configurator A PHP callable
788+
* @param string|array|Reference $configurator A PHP function, reference or an array containing a class/Reference and a method to call
787789
*
788790
* @return $this
789791
*/
@@ -793,6 +795,8 @@ public function setConfigurator($configurator)
793795

794796
if (\is_string($configurator) && false !== strpos($configurator, '::')) {
795797
$configurator = explode('::', $configurator, 2);
798+
} elseif ($configurator instanceof Reference) {
799+
$configurator = [$configurator, '__invoke'];
796800
}
797801

798802
$this->configurator = $configurator;

Loader/XmlFileLoader.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults)
317317
$class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null;
318318
}
319319

320-
$definition->setFactory([$class, $factory->getAttribute('method')]);
320+
$definition->setFactory([$class, $factory->getAttribute('method') ?: '__invoke']);
321321
}
322322
}
323323

@@ -332,7 +332,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults)
332332
$class = $configurator->getAttribute('class');
333333
}
334334

335-
$definition->setConfigurator([$class, $configurator->getAttribute('method')]);
335+
$definition->setConfigurator([$class, $configurator->getAttribute('method') ?: '__invoke']);
336336
}
337337
}
338338

Loader/YamlFileLoader.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,12 +573,15 @@ private function parseDefinition($id, $service, $file, array $defaults)
573573
*
574574
* @throws InvalidArgumentException When errors occur
575575
*
576-
* @return string|array A parsed callable
576+
* @return string|array|Reference A parsed callable
577577
*/
578578
private function parseCallable($callable, $parameter, $id, $file)
579579
{
580580
if (\is_string($callable)) {
581581
if ('' !== $callable && '@' === $callable[0]) {
582+
if (false === strpos($callable, ':')) {
583+
return [$this->resolveServices($callable, $file), '__invoke'];
584+
}
582585
throw new InvalidArgumentException(sprintf('The value of the "%s" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s").', $parameter, $id, $callable, substr($callable, 1)));
583586
}
584587

Tests/DefinitionTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Definition;
16+
use Symfony\Component\DependencyInjection\Reference;
1617

1718
class DefinitionTest extends TestCase
1819
{
@@ -35,6 +36,9 @@ public function testSetGetFactory()
3536

3637
$def->setFactory('Foo::bar');
3738
$this->assertEquals(['Foo', 'bar'], $def->getFactory(), '->setFactory() converts string static method call to the array');
39+
40+
$def->setFactory($ref = new Reference('baz'));
41+
$this->assertSame([$ref, '__invoke'], $def->getFactory(), '->setFactory() converts service reference to class invoke call');
3842
$this->assertSame(['factory' => true], $def->getChanges());
3943
}
4044

Tests/Fixtures/xml/services6.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
<service id="new_factory4" class="BazClass">
6060
<factory method="getInstance" />
6161
</service>
62+
<service id="new_factory5" class="FooBarClass">
63+
<factory service="baz" />
64+
</service>
6265
<service id="alias_for_foo" alias="foo" />
6366
<service id="another_alias_for_foo" alias="foo" public="false" />
6467
<service id="0" class="FooClass" />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
factory:
3+
class: Baz
4+
invalid_factory:
5+
class: FooBarClass
6+
factory: '@factory:method'

Tests/Fixtures/yaml/services14.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
services:
22
factory: { class: FooBarClass, factory: baz:getClass}
33
factory_with_static_call: { class: FooBarClass, factory: FooBacFactory::createFooBar}
4+
invokable_factory: { class: FooBarClass, factory: '@factory' }

Tests/Fixtures/yaml/services6.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ services:
3434
new_factory2: { class: FooBarClass, factory: ['@baz', getClass]}
3535
new_factory3: { class: FooBarClass, factory: [BazClass, getInstance]}
3636
new_factory4: { class: BazClass, factory: [~, getInstance]}
37+
new_factory5: { class: FooBarClass, factory: '@baz' }
3738
Acme\WithShortCutArgs: [foo, '@baz']
3839
alias_for_foo: '@foo'
3940
another_alias_for_foo:

Tests/Loader/XmlFileLoaderTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public function testLoadServices()
269269
$this->assertEquals([new Reference('baz'), 'getClass'], $services['new_factory2']->getFactory(), '->load() parses the factory tag');
270270
$this->assertEquals(['BazClass', 'getInstance'], $services['new_factory3']->getFactory(), '->load() parses the factory tag');
271271
$this->assertSame([null, 'getInstance'], $services['new_factory4']->getFactory(), '->load() accepts factory tag without class');
272+
$this->assertEquals([new Reference('baz'), '__invoke'], $services['new_factory5']->getFactory(), '->load() accepts service reference as invokable factory');
272273

273274
$aliases = $container->getAliases();
274275
$this->assertArrayHasKey('alias_for_foo', $aliases, '->load() parses <service> elements');

Tests/Loader/YamlFileLoaderTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
2222
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
2323
use Symfony\Component\DependencyInjection\ContainerBuilder;
24+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
2425
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
2526
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
2627
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
@@ -159,6 +160,7 @@ public function testLoadServices()
159160
$this->assertEquals([new Reference('baz'), 'getClass'], $services['new_factory2']->getFactory(), '->load() parses the factory tag');
160161
$this->assertEquals(['BazClass', 'getInstance'], $services['new_factory3']->getFactory(), '->load() parses the factory tag');
161162
$this->assertSame([null, 'getInstance'], $services['new_factory4']->getFactory(), '->load() accepts factory tag without class');
163+
$this->assertEquals([new Reference('baz'), '__invoke'], $services['new_factory5']->getFactory(), '->load() accepts service reference as invokable factory');
162164
$this->assertEquals(['foo', new Reference('baz')], $services['Acme\WithShortCutArgs']->getArguments(), '->load() parses short service definition');
163165

164166
$aliases = $container->getAliases();
@@ -197,6 +199,16 @@ public function testLoadFactoryShortSyntax()
197199

198200
$this->assertEquals([new Reference('baz'), 'getClass'], $services['factory']->getFactory(), '->load() parses the factory tag with service:method');
199201
$this->assertEquals(['FooBacFactory', 'createFooBar'], $services['factory_with_static_call']->getFactory(), '->load() parses the factory tag with Class::method');
202+
$this->assertEquals([new Reference('factory'), '__invoke'], $services['invokable_factory']->getFactory(), '->load() parses string service reference');
203+
}
204+
205+
public function testFactorySyntaxError()
206+
{
207+
$container = new ContainerBuilder();
208+
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
209+
$this->expectException(InvalidArgumentException::class);
210+
$this->expectExceptionMessage('The value of the "factory" option for the "invalid_factory" service must be the id of the service without the "@" prefix (replace "@factory:method" with "factory:method").');
211+
$loader->load('bad_factory_syntax.yml');
200212
}
201213

202214
public function testLoadConfiguratorShortSyntax()

0 commit comments

Comments
 (0)