Skip to content

Commit

Permalink
feature #30255 [DependencyInjection] Invokable Factory Services (zanb…
Browse files Browse the repository at this point in the history
…aldwin)

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

23cb83f [DependencyInjection] Invokable Factory Services
  • Loading branch information
fabpot committed Apr 3, 2019
2 parents 248aff5 + 23cb83f commit 4ad54da
Show file tree
Hide file tree
Showing 10 changed files with 40 additions and 5 deletions.
8 changes: 6 additions & 2 deletions src/Symfony/Component/DependencyInjection/Definition.php
Expand Up @@ -95,7 +95,7 @@ public function setChanges(array $changes)
/**
* 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
*/
Expand All @@ -105,6 +105,8 @@ public function setFactory($factory)

if (\is_string($factory) && false !== strpos($factory, '::')) {
$factory = explode('::', $factory, 2);
} elseif ($factory instanceof Reference) {
$factory = [$factory, '__invoke'];
}

$this->factory = $factory;
Expand Down Expand Up @@ -783,7 +785,7 @@ public function getDeprecationMessage($id)
/**
* 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
*/
Expand All @@ -793,6 +795,8 @@ public function setConfigurator($configurator)

if (\is_string($configurator) && false !== strpos($configurator, '::')) {
$configurator = explode('::', $configurator, 2);
} elseif ($configurator instanceof Reference) {
$configurator = [$configurator, '__invoke'];
}

$this->configurator = $configurator;
Expand Down
Expand Up @@ -317,7 +317,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults)
$class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null;
}

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

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

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

Expand Down
Expand Up @@ -573,12 +573,15 @@ private function parseDefinition($id, $service, $file, array $defaults)
*
* @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)));
}

Expand Down
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class DefinitionTest extends TestCase
{
Expand All @@ -35,6 +36,9 @@ public function testSetGetFactory()

$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());
}

Expand Down
Expand Up @@ -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" />
Expand Down
@@ -0,0 +1,6 @@
services:
factory:
class: Baz
invalid_factory:
class: FooBarClass
factory: '@factory:method'
@@ -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' }
Expand Up @@ -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:
Expand Down
Expand Up @@ -269,6 +269,7 @@ public function testLoadServices()
$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');
Expand Down
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -159,6 +160,7 @@ public function testLoadServices()
$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();
Expand Down Expand Up @@ -197,6 +199,16 @@ public function testLoadFactoryShortSyntax()

$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()
Expand Down

0 comments on commit 4ad54da

Please sign in to comment.