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

Skip to content

Commit 000b348

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

File tree

8 files changed

+303
-10
lines changed

8 files changed

+303
-10
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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
* @author Nikita Konstantinov <unk91nd@gmail.com>
18+
*
19+
* @api
20+
*/
21+
interface ClosureDumperInterface
22+
{
23+
/**
24+
* @param \Closure $closure
25+
* @return string
26+
*
27+
* @throws \Symfony\Component\DependencyInjection\Exception\DumpingClosureException If closure couldn't be dumped
28+
*/
29+
public function dump(\Closure $closure);
30+
}

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

Lines changed: 29 additions & 3 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
*
@@ -202,7 +218,7 @@ private function addProxyClasses()
202218
$code = '';
203219

204220
foreach ($definitions as $definition) {
205-
$code .= "\n" . $this->getProxyDumper()->getProxyCode($definition);
221+
$code .= "\n".$this->getProxyDumper()->getProxyCode($definition);
206222
}
207223

208224
return $code;
@@ -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

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

871-
return $code . " );\n";
897+
return $code." );\n";
872898
}
873899

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

899-
return $code . " );\n";
925+
return $code." );\n";
900926
}
901927

902928
/**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
* @author Nikita Konstantinov <unk91nd@gmail.com>
18+
*/
19+
final class DumpingClosureException extends \RuntimeException implements ExceptionInterface
20+
{
21+
public function __construct(\Closure $closure)
22+
{
23+
$reflection = new \ReflectionFunction($closure);
24+
25+
parent::__construct(sprintf(
26+
'Closure defined in %s at line %d could not be dumped',
27+
$reflection->getFileName(),
28+
$reflection->getStartLine()
29+
));
30+
}
31+
}

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

Expand all lines: src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,65 @@ 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+
393+
return $foo;
394+
});
395+
396+
$this->assertSame($builder->get('bar'), $builder->get('foo')->bar, '->createService() creates service from a closure with passed container as an argument');
397+
}
398+
399+
/**
400+
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
401+
*/
402+
public function testCreateServiceByClosureWithPassedServiceAsAnArgument()
403+
{
404+
$builder = new ContainerBuilder();
405+
$builder->register('bar', 'stdClass');
406+
$builder->register('foo', 'Bar\FooClass')->setFactoryMethod(function (\stdClass $bar) {
407+
$foo = new \Bar\FooClass();
408+
$foo->setBar($bar);
409+
410+
return $foo;
411+
})->addArgument(new Reference('bar'));
412+
413+
$this->assertSame($builder->get('bar'), $builder->get('foo')->bar, '->createService() creates service from a closure with passed service as an argument');
414+
}
415+
416+
/**
417+
* @covers Symfony\Component\DependencyInjection\ContainerBui 10000 lder::createService
418+
*/
419+
public function testCreateServiceWithInconsistentDefinition()
420+
{
421+
$expectedMessage = 'Definition of service "%s" is inconsistent (mixing of closure and factory service/class)';
422+
423+
$builder = new ContainerBuilder();
424+
$builder->register('foo', 'stdClass');
425+
$builder->register('bar', 'stdClass')->setFactoryService('foo')->setFactoryMethod(function () {});
426+
$builder->register('baz', 'stdClass')->setFactoryClass('stdClass')->setFactoryMethod(function () {});
427+
428+
try {
429+
$builder->get('bar');
430+
} catch (RuntimeException $e) {
431+
$this->assertEquals(sprintf($expectedMessage, 'bar'), $e->getMessage());
432+
}
433+
434+
try {
435+
$builder->get('baz');
436+
} catch (RuntimeException $e) {
437+
$this->assertEquals(sprintf($expectedMessage, 'baz'), $e->getMessage());
438+
}
439+
}
440+
382441
/**
383442
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::createService
384443
* @expectedException \RuntimeException
@@ -486,7 +545,7 @@ public function testfindTaggedServiceIds()
486545
'foo' => array(
487546
array('foo' => 'foo'),
488547
array('foofoo' => 'foofoo'),
489-
)
548+
),
490549
), '->findTaggedServiceIds() returns an array of service ids and its tag attributes');
491550
$this->assertEquals(array(), $builder->findTaggedServiceIds('foobar'), '->findTaggedServiceIds() returns an empty array if there is annotated services');
492551
}
@@ -652,7 +711,7 @@ public function testPrivateServiceUser()
652711

653712
$container->addDefinitions(array(
654713
'bar' => $fooDefinition,
655-
'bar_user' => $fooUserDefinition
714+
'bar_user' => $fooUserDefinition,
656715
));
657716

658717
$container->compile();

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

Lines changed: 79 additions & 1 deletion
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;
@@ -62,7 +64,7 @@ public function testDumpOptimizationString()
6264
'concatenation from the start value' => '\'\'.',
6365
'.' => 'dot as a key',
6466
'.\'\'.' => 'concatenation as a key',
65-
'\'\'.' =>'concatenation from the start key',
67+
'\'\'.' => 'concatenation from the start key',
6668
'optimize concatenation' => "string1%some_string%string2",
6769
'optimize concatenation with empty string' => "string1%empty_value%string2",
6870
'optimize concatenation from the start' => '%empty_value%start',
@@ -196,4 +198,80 @@ 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+
232+
return $bar;
233+
}
234+
CODE
235+
));
236+
237+
$closureDumperMock
238+
->expects($this->at(1))
239+
->method('dump')
240+
->will($this->returnValue(
241+
<<<'CODE'
242+
function (\Symfony\Component\DependencyInjection\ContainerInterface $container) {
243+
return new \stdClass();
244+
}
245+
CODE
246+
));
247+
248+
$dumper = new PhpDumper($container);
249+
$dumper->setClosureDumper($closureDumperMock);
250+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services12.php', $dumper->dump());
251+
}
252+
253+
/**
254+
* @expectedException \Symfony\Component\DependencyInjection\Exception\DumpingClosureException
255+
*/
256+
public function testUndumpableClosure()
257+
{
258+
// Depends on $this and couldn't be dumped
259+
$contextDependentClosure = function () {
260+
return $this;
261+
};
262+
263+
$container = new ContainerBuilder();
264+
$container->register('foo', 'stdClass')->setFactoryMethod($contextDependentClosure);
265+
266+
$closureDumperMock = $this->getMockForAbstractClass('Symfony\Component\DependencyInjection\Dumper\ClosureDumper\ClosureDumperInterface');
267+
268+
$closureDumperMock
269+
->expects($this->once())
270+
->method('dump')
271+
->will($this->throwException(new DumpingClosureException($contextDependentClosure)));
272+
273+
$dumper = new PhpDumper($container);
274+
$dumper->setClosureDumper($closureDumperMock);
275+
$dumper->dump();
276+
}
199277
}

0 commit comments

Comments
 (0)
0