Skip to content

Commit cde55c3

Browse files
GromNaNfabpot
authored andcommitted
[DependencyInjection] Allow Class::function(...) and global_function(...) closures in PHP DSL for factories
1 parent 1cdaa51 commit cde55c3

File tree

10 files changed

+243
-5
lines changed

10 files changed

+243
-5
lines changed

src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,29 @@ public static function processValue(mixed $value, bool $allowServices = false):
114114

115115
throw new InvalidArgumentException(\sprintf('Cannot use values of type "%s" in service configuration files.', get_debug_type($value)));
116116
}
117+
118+
/**
119+
* Converts a named closure to dumpable callable.
120+
*
121+
* @throws InvalidArgumentException if the closure is anonymous or references a non-static method
122+
*/
123+
final public static function processClosure(\Closure $closure): callable
124+
{
125+
$function = new \ReflectionFunction($closure);
126+
if ($function->isAnonymous()) {
127+
throw new InvalidArgumentException('Anonymous closure not supported. The closure must be created from a static method or a global function.');
128+
}
129+
130+
// Convert global_function(...) closure into 'global_function'
131+
if (!$class = $function->getClosureCalledClass()) {
132+
return $function->name;
133+
}
134+
135+
// Convert Class::method(...) closure into ['Class', 'method']
136+
if ($function->isStatic()) {
137+
return [$class->name, $function->name];
138+
}
139+
140+
throw new InvalidArgumentException(\sprintf('The method "%s::%s(...)" is not static.', $class->name, $function->name));
141+
}
117142
}

src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,12 @@ function service_closure(string $serviceId): ClosureReferenceConfigurator
192192
/**
193193
* Creates a closure.
194194
*/
195-
function closure(string|array|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
195+
function closure(string|array|\Closure|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
196196
{
197+
if ($callable instanceof \Closure) {
198+
$callable = AbstractConfigurator::processClosure($callable);
199+
}
200+
197201
return (new InlineServiceConfigurator(new Definition('Closure')))
198202
->factory(['Closure', 'fromCallable'])
199203
->args([$callable]);

src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/ConfiguratorTrait.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ trait ConfiguratorTrait
2020
*
2121
* @return $this
2222
*/
23-
final public function configurator(string|array|ReferenceConfigurator $configurator): static
23+
final public function configurator(string|array|\Closure|ReferenceConfigurator $configurator): static
2424
{
25+
if ($configurator instanceof \Closure) {
26+
$this->definition->setConfigurator(static::processClosure($configurator));
27+
28+
return $this;
29+
}
30+
2531
$this->definition->setConfigurator(static::processValue($configurator, true));
2632

2733
return $this;

src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ trait FactoryTrait
2222
*
2323
* @return $this
2424
*/
25-
final public function factory(string|array|ReferenceConfigurator|Expression $factory): static
25+
final public function factory(string|array|\Closure|ReferenceConfigurator|Expression $factory): static
2626
{
27+
if ($factory instanceof \Closure) {
28+
$this->definition->setFactory(static::processClosure($factory));
29+
30+
return $this;
31+
}
32+
2733
if (\is_string($factory) && 1 === substr_count($factory, ':')) {
2834
$factoryParts = explode(':', $factory);
2935

src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
trait FromCallableTrait
2121
{
22-
final public function fromCallable(string|array|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
22+
final public function fromCallable(string|array|\Closure|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
2323
{
2424
if ($this->definition instanceof ChildDefinition) {
2525
throw new InvalidArgumentException('The configuration key "parent" is unsupported when using "fromCallable()".');
@@ -41,6 +41,10 @@ final public function fromCallable(string|array|ReferenceConfigurator|Expression
4141

4242
$this->definition->setFactory(['Closure', 'fromCallable']);
4343

44+
if ($callable instanceof \Closure) {
45+
$callable = static::processClosure($callable);
46+
}
47+
4448
if (\is_string($callable) && 1 === substr_count($callable, ':')) {
4549
$parts = explode(':', $callable);
4650

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Testing\NamedClosure;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
7+
use function Symfony\Component\DependencyInjection\Loader\Configurator\closure;
8+
9+
interface NamedClosureInterface
10+
{
11+
public function theMethod();
12+
}
13+
14+
class NamedClosureClass
15+
{
16+
public function __construct(...$args)
17+
{
18+
}
19+
20+
public static function getInstance(): self
21+
{
22+
return new self();
23+
}
24+
25+
public static function configure(self $instance): void
26+
{
27+
}
28+
}
29+
30+
return function (ContainerConfigurator $c) {
31+
$c->services()
32+
->set('from_callable', NamedClosureInterface::class)
33+
->fromCallable(NamedClosureClass::getInstance(...))
34+
->public()
35+
->set('has_factory', NamedClosureClass::class)
36+
->factory(NamedClosureClass::getInstance(...))
37+
->public()
38+
->set('has_configurator', NamedClosureClass::class)
39+
->configurator(NamedClosureClass::configure(...))
40+
->public()
41+
->set('with_closure', NamedClosureClass::class)
42+
->args([closure(dirname(...))])
43+
->public()
44+
;
45+
};

src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/services9.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
->tag('foo', ['foo' => 'foo'])
2424
->tag('foo', ['bar' => 'bar', 'baz' => 'baz'])
2525
->tag('foo', ['name' => 'bar', 'baz' => 'baz'])
26-
->factory([FooClass::class, 'getInstance'])
26+
->factory(FooClass::getInstance(...))
2727
->property('foo', 'bar')
2828
->property('moo', service('foo.baz'))
2929
->property('qux', ['%foo%' => 'foo is %foo%', 'foobar' => '%foo%'])
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
4+
use Symfony\Component\DependencyInjection\ContainerInterface;
5+
use Symfony\Component\DependencyInjection\Container;
6+
use Symfony\Component\DependencyInjection\Exception\LogicException;
7+
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
8+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
9+
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
10+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
11+
12+
/**
13+
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
14+
*/
15+
class ProjectServiceContainer extends Container
16+
{
17+
protected $parameters = [];
18+
19+
public function __construct()
20+
{
21+
$this->services = $this->privates = [];
22+
$this->methodMap = [
23+
'from_callable' => 'getFromCallableService',
24+
'has_configurator' => 'getHasConfiguratorService',
25+
'has_factory' => 'getHasFactoryService',
26+
'with_closure' => 'getWithClosureService',
27+
];
28+
29+
$this->aliases = [];
30+
}
31+
32+
public function compile(): void
33+
{
34+
throw new LogicException('You cannot compile a dumped container that was already compiled.');
35+
}
36+
37+
public function isCompiled(): bool
38+
{
39+
return true;
40+
}
41+
42+
protected function createProxy($class, \Closure $factory)
43+
{
44+
return $factory();
45+
}
46+
47+
/**
48+
* Gets the public 'from_callable' shared service.
49+
*
50+
* @return \Testing\NamedClosure\NamedClosureInterface
51+
*/
52+
protected static function getFromCallableService($container, $lazyLoad = true)
53+
{
54+
return $container->services['from_callable'] = new class(fn () => 'Testing\\NamedClosure\\NamedClosureClass') extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Testing\NamedClosure\NamedClosureInterface { public function theMethod() { return $this->service->getInstance(...\func_get_args()); } };
55+
}
56+
57+
/**
58+
* Gets the public 'has_configurator' shared service.
59+
*
60+
* @return \Testing\NamedClosure\NamedClosureClass
61+
*/
62+
protected static function getHasConfiguratorService($container)
63+
{
64+
$container->services['has_configurator'] = $instance = new \Testing\NamedClosure\NamedClosureClass();
65+
66+
\Testing\NamedClosure\NamedClosureClass::configure($instance);
67+
68+
return $instance;
69+
}
70+
71+
/**
72+
* Gets the public 'has_factory' shared service.
73+
*
74+
* @return \Testing\NamedClosure\NamedClosureClass
75+
*/
76+
protected static function getHasFactoryService($container)
77+
{
78+
return $container->services['has_factory'] = \Testing\NamedClosure\NamedClosureClass::getInstance();
79+
}
80+
81+
/**
82+
* Gets the public 'with_closure' shared service.
83+
*
84+
* @return \Testing\NamedClosure\NamedClosureClass
85+
*/
86+
protected static function getWithClosureService($container)
87+
{
88+
return $container->services['with_closure'] = new \Testing\NamedClosure\NamedClosureClass(\dirname(...));
89+
}
90+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 Loader\Configurator;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator;
17+
18+
class AbstractConfiguratorTest extends TestCase
19+
{
20+
public function testProcessClosure()
21+
{
22+
$this->assertSame(
23+
[\DateTime::class, 'createFromFormat'],
24+
AbstractConfigurator::processClosure(\DateTime::createFromFormat(...)),
25+
);
26+
27+
$this->assertSame(
28+
'date_create',
29+
AbstractConfigurator::processClosure(date_create(...)),
30+
);
31+
}
32+
33+
public function testProcessNonStaticNamedClosure()
34+
{
35+
self::expectException(InvalidArgumentException::class);
36+
self::expectExceptionMessage('The method "DateTime::format(...)" is not static');
37+
38+
AbstractConfigurator::processClosure((new \DateTime())->format(...));
39+
}
40+
41+
public function testProcessAnonymousClosure()
42+
{
43+
self::expectException(InvalidArgumentException::class);
44+
self::expectExceptionMessage('Anonymous closure not supported. The closure must be created from a static method or a global function.');
45+
46+
AbstractConfigurator::processClosure(static fn () => new \DateTime());
47+
}
48+
}

src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,14 @@ public function testConfigBuilderEnvConfigurator()
309309

310310
$this->assertIsString($container->getExtensionConfig('acme')[0]['color']);
311311
}
312+
313+
public function testNamedClosure()
314+
{
315+
$container = new ContainerBuilder();
316+
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures/config'), 'some-env');
317+
$loader->load('named_closure.php');
318+
$container->compile();
319+
$dumper = new PhpDumper($container);
320+
$this->assertStringEqualsFile(\dirname(__DIR__).'/Fixtures/php/named_closure_compiled.php', $dumper->dump());
321+
}
312322
}

0 commit comments

Comments
 (0)