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
This commit is contained in:
Fabien Potencier 2019-04-03 14:19:39 +02:00
commit 4ad54dad28
10 changed files with 40 additions and 5 deletions

View File

@ -95,7 +95,7 @@ class Definition
/**
* Sets a factory.
*
* @param string|array $factory A PHP function or an array containing a class/Reference and a method to call
* @param string|array|Reference $factory A PHP function, reference or an array containing a class/Reference and a method to call
*
* @return $this
*/
@ -105,6 +105,8 @@ class Definition
if (\is_string($factory) && false !== strpos($factory, '::')) {
$factory = explode('::', $factory, 2);
} elseif ($factory instanceof Reference) {
$factory = [$factory, '__invoke'];
}
$this->factory = $factory;
@ -783,7 +785,7 @@ class Definition
/**
* Sets a configurator to call after the service is fully initialized.
*
* @param string|array $configurator A PHP callable
* @param string|array|Reference $configurator A PHP function, reference or an array containing a class/Reference and a method to call
*
* @return $this
*/
@ -793,6 +795,8 @@ class Definition
if (\is_string($configurator) && false !== strpos($configurator, '::')) {
$configurator = explode('::', $configurator, 2);
} elseif ($configurator instanceof Reference) {
$configurator = [$configurator, '__invoke'];
}
$this->configurator = $configurator;

View File

@ -317,7 +317,7 @@ class XmlFileLoader extends FileLoader
$class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null;
}
$definition->setFactory([$class, $factory->getAttribute('method')]);
$definition->setFactory([$class, $factory->getAttribute('method') ?: '__invoke']);
}
}
@ -332,7 +332,7 @@ class XmlFileLoader extends FileLoader
$class = $configurator->getAttribute('class');
}
$definition->setConfigurator([$class, $configurator->getAttribute('method')]);
$definition->setConfigurator([$class, $configurator->getAttribute('method') ?: '__invoke']);
}
}

View File

@ -573,12 +573,15 @@ class YamlFileLoader extends FileLoader
*
* @throws InvalidArgumentException When errors occur
*
* @return string|array A parsed callable
* @return string|array|Reference A parsed callable
*/
private function parseCallable($callable, $parameter, $id, $file)
{
if (\is_string($callable)) {
if ('' !== $callable && '@' === $callable[0]) {
if (false === strpos($callable, ':')) {
return [$this->resolveServices($callable, $file), '__invoke'];
}
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)));
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class DefinitionTest extends TestCase
{
@ -35,6 +36,9 @@ class DefinitionTest extends TestCase
$def->setFactory('Foo::bar');
$this->assertEquals(['Foo', 'bar'], $def->getFactory(), '->setFactory() converts string static method call to the array');
$def->setFactory($ref = new Reference('baz'));
$this->assertSame([$ref, '__invoke'], $def->getFactory(), '->setFactory() converts service reference to class invoke call');
$this->assertSame(['factory' => true], $def->getChanges());
}

View File

@ -59,6 +59,9 @@
<service id="new_factory4" class="BazClass">
<factory method="getInstance" />
</service>
<service id="new_factory5" class="FooBarClass">
<factory service="baz" />
</service>
<service id="alias_for_foo" alias="foo" />
<service id="another_alias_for_foo" alias="foo" public="false" />
<service id="0" class="FooClass" />

View File

@ -0,0 +1,6 @@
services:
factory:
class: Baz
invalid_factory:
class: FooBarClass
factory: '@factory:method'

View File

@ -1,3 +1,4 @@
services:
factory: { class: FooBarClass, factory: baz:getClass}
factory_with_static_call: { class: FooBarClass, factory: FooBacFactory::createFooBar}
invokable_factory: { class: FooBarClass, factory: '@factory' }

View File

@ -34,6 +34,7 @@ services:
new_factory2: { class: FooBarClass, factory: ['@baz', getClass]}
new_factory3: { class: FooBarClass, factory: [BazClass, getInstance]}
new_factory4: { class: BazClass, factory: [~, getInstance]}
new_factory5: { class: FooBarClass, factory: '@baz' }
Acme\WithShortCutArgs: [foo, '@baz']
alias_for_foo: '@foo'
another_alias_for_foo:

View File

@ -269,6 +269,7 @@ class XmlFileLoaderTest extends TestCase
$this->assertEquals([new Reference('baz'), 'getClass'], $services['new_factory2']->getFactory(), '->load() parses the factory tag');
$this->assertEquals(['BazClass', 'getInstance'], $services['new_factory3']->getFactory(), '->load() parses the factory tag');
$this->assertSame([null, 'getInstance'], $services['new_factory4']->getFactory(), '->load() accepts factory tag without class');
$this->assertEquals([new Reference('baz'), '__invoke'], $services['new_factory5']->getFactory(), '->load() accepts service reference as invokable factory');
$aliases = $container->getAliases();
$this->assertArrayHasKey('alias_for_foo', $aliases, '->load() parses <service> elements');

View File

@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
@ -159,6 +160,7 @@ class YamlFileLoaderTest extends TestCase
$this->assertEquals([new Reference('baz'), 'getClass'], $services['new_factory2']->getFactory(), '->load() parses the factory tag');
$this->assertEquals(['BazClass', 'getInstance'], $services['new_factory3']->getFactory(), '->load() parses the factory tag');
$this->assertSame([null, 'getInstance'], $services['new_factory4']->getFactory(), '->load() accepts factory tag without class');
$this->assertEquals([new Reference('baz'), '__invoke'], $services['new_factory5']->getFactory(), '->load() accepts service reference as invokable factory');
$this->assertEquals(['foo', new Reference('baz')], $services['Acme\WithShortCutArgs']->getArguments(), '->load() parses short service definition');
$aliases = $container->getAliases();
@ -197,6 +199,16 @@ class YamlFileLoaderTest extends TestCase
$this->assertEquals([new Reference('baz'), 'getClass'], $services['factory']->getFactory(), '->load() parses the factory tag with service:method');
$this->assertEquals(['FooBacFactory', 'createFooBar'], $services['factory_with_static_call']->getFactory(), '->load() parses the factory tag with Class::method');
$this->assertEquals([new Reference('factory'), '__invoke'], $services['invokable_factory']->getFactory(), '->load() parses string service reference');
}
public function testFactorySyntaxError()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$this->expectException(InvalidArgumentException::class);
$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").');
$loader->load('bad_factory_syntax.yml');
}
public function testLoadConfiguratorShortSyntax()