Skip to content

[DependencyInjection] Added support for closure as a factory method #11968

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,13 @@ public function createService(Definition $definition, $id, $tryProxy = true)

$arguments = $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())));

if (null !== $definition->getFactoryMethod()) {
if ($definition->getFactoryMethod() instanceof \Closure) {
if (null !== $definition->getFactoryService() || null !== $definition->getFactoryClass()) {
throw new RuntimeException(sprintf('Definition of service "%s" is inconsistent (mixing of closure and factory service/class)', $id));
}

$service = call_user_func_array($definition->getFactoryMethod(), $arguments ? $arguments : array($this));
} elseif (null !== $definition->getFactoryMethod()) {
if (null !== $definition->getFactoryClass()) {
$factory = $parameterBag->resolveValue($definition->getFactoryClass());
} elseif (null !== $definition->getFactoryService()) {
Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Component/DependencyInjection/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public function getFactoryClass()
/**
* Sets the factory method able to create an instance of this class.
*
* @param string $factoryMethod The factory method name
* @param string|\Closure $factoryMethod The factory method name or closure
*
* @return Definition The current instance
*
Expand All @@ -109,7 +109,7 @@ public function setFactoryMethod($factoryMethod)
*
* @return Definition The current instance
*
* @throws InvalidArgumentException In case the decorated service id and the new decorated service id are equals.
* @throws \InvalidArgumentException In case the decorated service id and the new decorated service id are equals.
*/
public function setDecoratedService($id, $renamedId = null)
{
Expand Down Expand Up @@ -139,7 +139,7 @@ public function getDecoratedService()
/**
* Gets the factory method.
*
* @return string|null The factory method name
* @return string|\Closure|null The factory method name or closure
*
* @api
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Dumper\ClosureDumper;

/**
* Interface of closure dumper
*
* @author Nikita Konstantinov <unk91nd@gmail.com>
*
* @api
*/
interface ClosureDumperInterface
{
/**
* @param \Closure $closure
* @return string
*
* @throws \Symfony\Component\DependencyInjection\Exception\DumpingClosureException If closure couldn't be dumped
*/
public function dump(\Closure $closure);
}
32 changes: 29 additions & 3 deletions src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\DependencyInjection\Dumper;

use Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface;
use Symfony\Component\DependencyInjection\Variable;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -60,6 +61,11 @@ class PhpDumper extends Dumper
*/
private $proxyDumper;

/**
* @var \Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface
*/
private $closureDumper;

/**
* {@inheritdoc}
*
Expand All @@ -82,6 +88,16 @@ public function setProxyDumper(ProxyDumper $proxyDumper)
$this->proxyDumper = $proxyDumper;
}

/**
* Sets the dumper of closures
*
* @param ClosureDumperInterface $closureDumper
*/
public function setClosureDumper(ClosureDumperInterface $closureDumper)
{
$this->closureDumper = $closureDumper;
}

/**
* Dumps the service container as a PHP class.
*
Expand Down Expand Up @@ -202,7 +218,7 @@ private function addProxyClasses()
$code = '';

foreach ($definitions as $definition) {
$code .= "\n" . $this->getProxyDumper()->getProxyCode($definition);
$code .= "\n".$this->getProxyDumper()->getProxyCode($definition);
}

return $code;
Expand Down Expand Up @@ -701,6 +717,16 @@ private function addNewInstance($id, Definition $definition, $return, $instantia
}

if (null !== $definition->getFactoryMethod()) {
if ($definition->getFactoryMethod() instanceof \Closure) {
if ($this->closureDumper === null) {
throw new RuntimeException('DIC PhpDumper requires a ClosureParserInterface implementation set in order to dump closures');
}

$closureCode = $this->closureDumper->dump($definition->getFactoryMethod());

return sprintf(" $return{$instantiation}call_user_func(%s, %s);\n", $closureCode, $arguments ? implode(', ', $arguments) : '$this');
}

if (null !== $definition->getFactoryClass()) {
$class = $this->dumpValue($definition->getFactoryClass());

Expand Down Expand Up @@ -868,7 +894,7 @@ private function addMethodMap()
$code .= ' '.var_export($id, true).' => '.var_export('get'.$this->camelize($id).'Service', true).",\n";
}

return $code . " );\n";
return $code." );\n";
}

/**
Expand Down Expand Up @@ -896,7 +922,7 @@ private function addAliases()
$code .= ' '.var_export($alias, true).' => '.var_export($id, true).",\n";
}

return $code . " );\n";
return $code." );\n";
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Exception;

/**
* This exception is thrown when closure is not dumpable, e.g. if closure depends on context
*
* @author Nikita Konstantinov <unk91nd@gmail.com>
*/
final class DumpingClosureException extends \RuntimeException implements ExceptionInterface
{
public function __construct(\Closure $closure)
{
$reflection = new \ReflectionFunction($closure);

parent::__construct(sprintf(
'Closure defined in %s at line %d could not be dumped',
$reflection->getFileName(),
$reflection->getStartLine()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,68 @@ public function testCreateServiceConfigurator()
}
}

/**
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
*/
public function testCreateServiceByClosureWithPassedContainerAsAnArgument()
{
$builder = new ContainerBuilder();
$builder->register('bar', 'stdClass');
$builder->register('foo', 'Bar\FooClass')->setFactoryMethod(function (ContainerInterface $container) {
$foo = new \Bar\FooClass();
$foo->setBar($container->get('bar'));

return $foo;
});

$this->assertSame($builder->get('bar'), $builder->get('foo')->bar, '->createService() creates service from a closure with passed container as an argument');
}

/**
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
*/
public function testCreateServiceByClosureWithPassedServiceAsAnArgument()
{
$builder = new ContainerBuilder();
$builder->register('bar', 'stdClass');
$builder->register('foo', 'Bar\FooClass')->setFactoryMethod(function (\stdClass $bar) {
$foo = new \Bar\FooClass();
$foo->setBar($bar);

return $foo;
})->addArgument(new Reference('bar'));

$this->assertSame($builder->get('bar'), $builder->get('foo')->bar, '->createService() creates service from a closure with passed service as an argument');
}

/**
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
*/
public function testCreateServiceWithInconsistentDefinition()
{
$expectedMessage = 'Definition of service "%s" is inconsistent (mixing of closure and factory service/class)';
$assertionMessage = '->createService() does not allow mixing of closure and factory service/class';

$builder = new ContainerBuilder();
$builder->register('foo', 'stdClass');
$builder->register('bar', 'stdClass')->setFactoryService('foo')->setFactoryMethod(function () {});
$builder->register('baz', 'stdClass')->setFactoryClass('stdClass')->setFactoryMethod(function () {});

try {
$builder->get('bar');
$this->fail($assertionMessage);
} catch (RuntimeException $e) {
$this->assertEquals(sprintf($expectedMessage, 'bar'), $e->getMessage());
}

try {
$builder->get('baz');
$this->fail($assertionMessage);
} catch (RuntimeException $e) {
$this->assertEquals(sprintf($expectedMessage, 'baz'), $e->getMessage());
}
}

/**
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
* @expectedException \RuntimeException
Expand Down Expand Up @@ -486,7 +548,7 @@ public function testfindTaggedServiceIds()
'foo' => array(
array('foo' => 'foo'),
array('foofoo' => 'foofoo'),
)
),
), '->findTaggedServiceIds() returns an array of service ids and its tag attributes');
$this->assertEquals(array(), $builder->findTaggedServiceIds('foobar'), '->findTaggedServiceIds() returns an empty array if there is annotated services');
}
Expand Down Expand Up @@ -652,7 +714,7 @@ public function testPrivateServiceUser()

$container->addDefinitions(array(
'bar' => $fooDefinition,
'bar_user' => $fooUserDefinition
'bar_user' => $fooUserDefinition,
));

$container->compile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
namespace Symfony\Component\DependencyInjection\Tests\Dumper;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Exception\DumpingClosureException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
Expand Down Expand Up @@ -62,7 +64,7 @@ public function testDumpOptimizationString()
'concatenation from the start value' => '\'\'.',
'.' => 'dot as a key',
'.\'\'.' => 'concatenation as a key',
'\'\'.' =>'concatenation from the start key',
'\'\'.' => 'concatenation from the start key',
'optimize concatenation' => "string1%some_string%string2",
'optimize concatenation with empty string' => "string1%empty_value%string2",
'optimize concatenation from the start' => '%empty_value%start',
Expand Down Expand Up @@ -196,4 +198,80 @@ public function testCircularReference()
$dumper = new PhpDumper($container);
$dumper->dump();
}

public function testClosureAsFactoryMethod()
{
$container = new ContainerBuilder();

$container->register('foo', 'stdClass')->setFactoryMethod(
function (ContainerInterface $container) {
return new \stdClass();
}
);

$container->register('bar', 'stdClass')->setFactoryMethod(
function (\stdClass $foo) {
$bar = clone $foo;
$bar->bar = 42;

return $bar;
}
)->addArgument(new Reference('foo'));

$closureDumperMock = $this->getMockForAbstractClass('Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface');

$closureDumperMock
->expects($this->at(0))
->method('dump')
->will($this->returnValue(
<<<'CODE'
function (\stdClass $foo) {
$bar = clone $foo;
$bar->bar = 42;

return $bar;
}
CODE
));

$closureDumperMock
->expects($this->at(1))
->method('dump')
->will($this->returnValue(
<<<'CODE'
function (\Symfony\Component\DependencyInjection\ContainerInterface $container) {
return new \stdClass();
}
CODE
));

$dumper = new PhpDumper($container);
$dumper->setClosureDumper($closureDumperMock);
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services12.php', $dumper->dump());
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\DumpingClosureException
*/
public function testUndumpableClosure()
{
// Depends on $this and couldn't be dumped
$contextDependentClosure = function () {
return $this;
};

$container = new ContainerBuilder();
$container->register('foo', 'stdClass')->setFactoryMethod($contextDependentClosure);

$closureDumperMock = $this->getMockForAbstractClass('Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface');

$closureDumperMock
->expects($this->once())
->method('dump')
->will($this->throwException(new DumpingClosureException($contextDependentClosure)));

$dumper = new PhpDumper($container);
$dumper->setClosureDumper($closureDumperMock);
$dumper->dump();
}
}
Loading