8000 [DependencyInjection] Added support for closure as a factory method · symfony/symfony@7ff4be2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7ff4be2

Browse files
committed
[DependencyInjection] Added support for closure as a factory method
1 parent a469c56 commit 7ff4be2

File tree

8 files changed

+289
-4
lines changed

8 files changed

+289
-4
lines changed

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,13 @@ public function createService(Definition $definition, $id, $tryProxy = true)
939939

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

942-
if (null !== $definition->getFactoryMethod()) {
942+
if ($definition->getFactoryMethod() instanceof \Closure) {
943+
if (null !== $definition->getFactoryService() || null !== $definition->getFactoryClass()) {
944+
throw new RuntimeException(sprintf('Definition of service "%s" is inconsistent (mixing of closure and factory service/class)', $id));
945+
}
946+
947+
$service = call_user_func_array($definition->getFactoryMethod(), $arguments ? $arguments : array($this));
948+
} elseif (null !== $definition->getFactoryMethod()) {
943949
if (null !== $definition->getFactoryClass()) {
944950
$factory = $parameterBag->resolveValue($definition->getFactoryClass());
945951
} elseif (null !== $definition->getFactoryService()) {

src/Symfony/Component/DependencyInjection/Definition.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function getFactoryClass()
8888
/**
8989
* Sets the factory method able to create an instance of this class.
9090
*
91-
* @param string $factoryMethod The factory method name
91+
* @param string|\Closure $factoryMethod The factory method name or closure
9292
*
9393
* @return Definition The current instance
9494
*
@@ -109,7 +109,7 @@ public function setFactoryMethod($factoryMethod)
109109
*
110110
* @return Definition The current instance
111111
*
112-
* @throws InvalidArgumentException In case the decorated service id and the new decorated service id are equals.
112+
* @throws \InvalidArgumentException In case the decorated service id and the new decorated service id are equals.
113113
*/
114114
public function setDecoratedService($id, $renamedId = null)
115115
{
@@ -139,7 +139,7 @@ public function getDecoratedService()
139139
/**
140140
* Gets the factory method.
141141
*
142-
* @return string|null The factory method name
142+
* @return string|\Closure|null The factory method name or closure
143143
*
144144
* @api
145145
*/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Dumper\ClosureDumper;
13+
14+
/**
15+
* Interface of closure dumper
16+
*
17+
* @api
18+
*/
19+
interface ClosureDumperInterface
20+
{
21+
/**
22+
* @param \Closure $closure
23+
* @return string
24+
*
25+
* @throws \Symfony\Component\DependencyInjection\Exception\DumpingClosureException If closure couldn't be dumped
26+
*/
27+
public function dump(\Closure $closure);
28+
}

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Dumper;
1313

14+
use Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface;
1415
use Symfony\Component\DependencyInjection\Variable;
1516
use Symfony\Component\DependencyInjection\Definition;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -60,6 +61,11 @@ class PhpDumper extends Dumper
6061
*/
6162
private $proxyDumper;
6263

64+
/**
65+
* @var \Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface
66+
*/
67+
private $closureDumper;
68+
6369
/**
6470
* {@inheritdoc}
6571
*
@@ -82,6 +88,16 @@ public function setProxyDumper(ProxyDumper $proxyDumper)
8288
$this->proxyDumper = $proxyDumper;
8389
}
8490

91+
/**
92+
* Sets the dumper of closures
93+
*
94+
* @param ClosureDumperInterface $closureDumper
95+
*/
96+
public function setClosureDumper(ClosureDumperInterface $closureDumper)
97+
{
98+
$this->closureDumper = $closureDumper;
99+
}
100+
85101
/**
86102
* Dumps the service container as a PHP class.
87103
*
@@ -701,6 +717,16 @@ private function addNewInstance($id, Definition $definition, $return, $instantia
701717
}
702718

703719
if (null !== $definition->getFactoryMethod()) {
720+
if ($definition->getFactoryMethod() instanceof \Closure) {
721+
if ($this->closureDumper === null) {
722+
throw new RuntimeException('DIC PhpDumper requires a ClosureParserInterface implementation set in order to dump closures');
723+
}
724+
725+
$closureCode = $this->closureDumper->dump($definition->getFactoryMethod());
726+
727+
return sprintf(" $return{$instantiation}call_user_func(%s, %s);\n", $closureCode, $arguments ? implode(', ', $arguments) : '$this');
728+
}
729+
704730
if (null !== $definition->getFactoryClass()) {
705731
$class = $this->dumpValue($definition->getFactoryClass());
706732

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Exception;
13+
14+
/**
15+
* This exception is thrown when closure is not dumpable, e.g. if closure depends on context
16+
*/
17+
final class DumpingClosureException extends \RuntimeException implements ExceptionInterface
18+
{
19+
public function __construct(\Closure $closure)
20+
{
21+
$reflection = new \ReflectionFunction($closure);
22+
23+
parent::__construct(sprintf(
24+
'Closure defined in %s at line %d could not be dumped',
25+
$reflection->getFileName(),
26+
$reflection->getStartLine()
27+
));
28+
}
29+
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,63 @@ public function testCreateServiceConfigurator()
379379
}
380380
}
381381

382+
/**
383+
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
384+
*/
385+
public function testCreateServiceByClosureWithPassedContainerAsAnArgument()
386+
{
387+
$builder = new ContainerBuilder();
388+
$builder->register('bar', 'stdClass');
389+
$builder->register('foo', 'Bar\FooClass')->setFactoryMethod(function (ContainerInterface $container) {
390+
$foo = new \Bar\FooClass();
391+
$foo->setBar($container->get('bar'));
392+
return $foo;
393+
});
394+
395+
$this->assertSame($builder->get('bar'), $builder->get('foo')->bar, '->createService() creates service from a closure with passed container as an argument');
396+
}
397+
398+
/**
399+
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
400+
*/
401+
public function testCreateServiceByClosureWithPassedServiceAsAnArgument()
402+
{
403+
$builder = new ContainerBuilder();
404+
$builder->register('bar', 'stdClass');
405+
$builder->register('foo', 'Bar\FooClass')->setFactoryMethod(function (\stdClass $bar) {
406+
$foo = new \Bar\FooClass();
407+
$foo->setBar($bar);
408+
return $foo;
409+
})->addArgument(new Reference('bar'));
410+
411+
$this->assertSame($builder->get('bar'), $builder->get('foo')->bar, '->createService() creates service from a closure with passed service as an argument');
412+
}
413+
414+
/**
415+
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
416+
*/
417+
public function testCreateServiceWithInconsistentDefinition()
418+
{
419+
$expectedMessage = 'Definition of service "%s" is inconsistent (mixing of closure and factory service/class)';
420+
421+
$builder = new ContainerBuilder();
422+
$builder->register('foo', 'stdClass');
423+
$builder->register('bar', 'stdClass')->setFactoryService('foo')->setFactoryMethod(function () {});
424+
$builder->register('baz', 'stdClass')->setFactoryClass('stdClass')->setFactoryMethod(function () {});
425+
426+
try {
427+
$builder->get('bar');
428+
} catch (RuntimeException $e) {
429+
$this->assertEquals(sprintf($expectedMessage, 'bar'), $e->getMessage());
430+
}
431+
432+
try {
433+
$builder->get('baz');
434+
} catch (RuntimeException $e) {
435+
$this->assertEquals(sprintf($expectedMessage, 'baz'), $e->getMessage());
436+
}
437+
}
438+
382439
/**
383440
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
384441
* @expectedException \RuntimeException

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
namespace Symfony\Component\DependencyInjection\Tests\Dumper;
1313

1414
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\ContainerInterface;
1516
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
17+
use Symfony\Component\DependencyInjection\Exception\DumpingClosureException;
1618
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
1719
use Symfony\Component\DependencyInjection\Reference;
1820
use Symfony\Component\DependencyInjection\Definition;
@@ -196,4 +198,79 @@ public function testCircularReference()
196198
$dumper = new PhpDumper($container);
197199
$dumper->dump();
198200
}
201+
202+
public function testClosureAsFactoryMethod()
203+
{
204+
$container = new ContainerBuilder();
205+
206+
$container->register('foo', 'stdClass')->setFactoryMethod(
207+
function (ContainerInterface $container) {
208+
return new \stdClass();
209+
}
210+
);
211+
212+
$container->register('bar', 'stdClass')->setFactoryMethod(
213+
function (\stdClass $foo) {
214+
$bar = clone $foo;
215+
$bar->bar = 42;
216+
217+
return $bar;
218+
}
219+
)->addArgument(new Reference('foo'));
220+
221+
$closureDumperMock = $this->getMockForAbstractClass('Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface');
222+
223+
$closureDumperMock
224+
->expects($this->at(0))
225+
->method('dump')
226+
->will($this->returnValue(
227+
<<<'CODE'
228+
function (\stdClass $foo) {
229+
$bar = clone $foo;
230+
$bar->bar = 42;
231+
return $bar;
232+
}
233+
CODE
234+
));
235+
236+
$closureDumperMock
237+
->expects($this->at(1))
238+
->method('dump')
239+
->will($this->returnValue(
240+
<<<'CODE'
241+
function (\Symfony\Component\DependencyInjection\ContainerInterface $container) {
242+
return new \stdClass();
243+
}
244+
CODE
245+
));
246+
247+
$dumper = new PhpDumper($container);
248+
$dumper->setClosureDumper($closureDumperMock);
249+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services12.php', $dumper->dump());
250+
}
251+
252+
/**
253+
* @expectedException \Symfony\Component\DependencyInjection\Exception\DumpingClosureException
254+
*/
255+
public function testUndumpableClosure()
256+
{
257+
// Depends on $this and couldn't be dumped
258+
$contextDependentClosure = function () {
259+
return $this;
260+
};
261+
262+
$container = new ContainerBuilder();
263+
$container->register('foo', 'stdClass')->setFactoryMethod($contextDependentClosure);
264+
265+
$closureDumperMock = $this->getMockForAbstractClass('Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface');
266+
267+
$closureDumperMock
268+
->expects($this->once())
269+
->method('dump')
270+
->will($this->throwException(new DumpingClosureException($contextDependentClosure)));
271+
272+
$dumper = new PhpDumper($container);
273+
$dumper->setClosureDumper($closureDumperMock);
274+
$dumper->dump();
275+
}
199276
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\ContainerInterface;
4+
use Symfony\Component\DependencyInjection\Container;
5+
use Symfony\Component\DependencyInjection\Exception\InactiveScopeException;
6+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
7+
use Symfony\Component\DependencyInjection\Exception\LogicException;
8+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
9+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
10+
11+
/**
12+
* ProjectServiceContainer
13+
*
14+
* This class has been auto-generated
15+
* by the Symfony Dependency Injection Component.
16+
*/
17+
class ProjectServiceContainer extends Container
18+
{
19+
/**
20+
* Constructor.
21+
*/
22+
public function __construct()
23+
{
24+
parent::__construct();
25+
$this->methodMap = array(
26+
'bar' => 'getBarService',
27+
'foo' => 'getFooService',
28+
);
29+
}
30+
31+
/**
32+
* Gets the 'bar' service.
33+
*
34+
* This service is shared.
35+
* This method always returns the same instance of the service.
36+
*
37+
* @return \stdClass A stdClass instance.
38+
*/
39+
protected function getBarService()
40+
{
41+
return $this->services['bar'] = call_user_func(function (\stdClass $foo) {
42+
$bar = clone $foo;
43+
$bar->bar = 42;
44+
return $bar;
45+
}, $this->get('foo'));
46+
}
47+
48+
/**
49+
* Gets the 'foo' service.
50+
*
51+
* This service is shared.
52+
* This method always returns the same instance of the service.
53+
*
54+
* @return \stdClass A stdClass instance.
55+
*/
56+
protected function getFooService()
57+
{
58+
return $this->services['foo'] = call_user_func(function (\Symfony\Component\DependencyInjection\ContainerInterface $container) {
59+
return new \stdClass();
60+
}, $this);
61+
}
62+
}

0 commit comments

Comments
 (0)
0