Skip to content

Commit be26be8

Browse files
[DependencyInjection] Make it possible to cast callables into single-method interfaces
1 parent c375406 commit be26be8

24 files changed

+514
-59
lines changed

src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php

+36-7
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Argument;
1313

14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Definition;
1416
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1517
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
18+
use Symfony\Component\DependencyInjection\Reference;
1619
use Symfony\Component\VarExporter\ProxyHelper;
1720

1821
/**
@@ -44,17 +47,43 @@ public function __get(mixed $name): mixed
4447
return $this->service;
4548
}
4649

47-
public static function getCode(string $initializer, ?\ReflectionClass $r, string $method, ?string $id): string
50+
public static function getCode(string $initializer, array $callable, Definition $definition, ContainerBuilder $container, ?string $id): string
4851
{
49-
if (!$r || !$r->hasMethod($method)) {
52+
$method = $callable[1];
53+
$asClosure = 'Closure' === ($definition->getClass() ?: 'Closure');
54+
55+
if ($asClosure) {
56+
$class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass();
57+
} else {
58+
$class = $definition->getClass();
59+
}
60+
61+
$r = $container->getReflectionClass($class);
62+
63+
if (!$asClosure) {
64+
if (!$r || !$r->isInterface()) {
65+
throw new RuntimeException(sprintf('Cannot create adapter for service "%s" because "%s" is not an interface.', $id, $class));
66+
}
67+
if (1 !== \count($method = $r->getMethods())) {
68+
throw new RuntimeException(sprintf('Cannot create adapter for service "%s" because interface "%s" doesn\'t have exactly one method.', $id, $class));
69+
}
70+
$method = $method[0]->name;
71+
} elseif (!$r || !$r->hasMethod($method)) {
5072
throw new RuntimeException(sprintf('Cannot create lazy closure for service "%s" because its corresponding callable is invalid.', $id));
5173
}
5274

53-
$signature = ProxyHelper::exportSignature($r->getMethod($method));
54-
$signature = preg_replace('/: static$/', ': \\'.$r->name, $signature);
75+
$code = ProxyHelper::exportSignature($r->getMethod($method));
76+
77+
if ($asClosure) {
78+
$code = ' { '.preg_replace('/: static$/', ': \\'.$r->name, $code);
79+
} else {
80+
$code = ' implements \\'.$r->name.' { '.$code;
81+
}
82+
83+
$code = 'new class('.$initializer.') extends \\'.self::class
84+
.$code.' { return $this->service->'.$callable[1].'(...\func_get_args()); } '
85+
.'}';
5586

56-
return '(new class('.$initializer.') extends \\'.self::class.' { '
57-
.$signature.' { return $this->service->'.$method.'(...\func_get_args()); } '
58-
.'})->'.$method.'(...)';
87+
return $asClosure ? '('.$code.')->'.$method.'(...)' : $code;
5988
}
6089
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ CHANGELOG
1818
* Add support for generating lazy closures
1919
* Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`
2020
* Add support for `#[Autowire(lazy: true)]`
21+
* Make it possible to cast callables into single-method interfaces
2122
* Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
2223
* Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead
2324

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,10 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
291291
$value = $this->processValue(new TypedReference($type ?: '?', $type ?: 'mixed', $invalidBehavior, $name, [$attribute, ...$target]));
292292

293293
if ($attribute instanceof AutowireCallable) {
294-
$value = (new Definition('Closure'))
294+
$value = (new Definition($type ?: 'Closure'))
295295
->setFactory(['Closure', 'fromCallable'])
296-
->setArguments([$value + [1 => '__invoke']])
297-
->setLazy($attribute->lazy);
296+
->setArguments([\is_array($value) ? $value + [1 => '__invoke'] : $value])
297+
->setLazy($attribute->lazy || 'Closure' !== ($type ?: 'Closure') && 'callable' !== (string) $parameter->getType());
298298
} elseif ($attribute->lazy && ($value instanceof Reference ? !$this->container->has($value) || !$this->container->findDefinition($value)->isLazy() : null === $attribute->value && $type)) {
299299
$this->container->register('.lazy.'.$value ??= $getValue(), $type)
300300
->setFactory('current')

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+4-5
Original file line numberDiff line numberDiff line change
@@ -1051,9 +1051,9 @@ private function createService(Definition $definition, array &$inlineServices, b
10511051
}
10521052

10531053
$parameterBag = $this->getParameterBag();
1054-
$class = ($parameterBag->resolveValue($definition->getClass()) ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null));
1054+
$class = $parameterBag->resolveValue($definition->getClass()) ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null);
10551055

1056-
if ('Closure' === $class && $definition->isLazy() && ['Closure', 'fromCallable'] === $definition->getFactory()) {
1056+
if (['Closure', 'fromCallable'] === $definition->getFactory() && ('Closure' !== $class || $definition->isLazy())) {
10571057
$callable = $parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArgument(0)));
10581058

10591059
if ($callable instanceof Reference || $callable instanceof Definition) {
@@ -1065,19 +1065,18 @@ private function createService(Definition $definition, array &$inlineServices, b
10651065
|| $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
10661066
)) {
10671067
$containerRef = $this->containerRef ??= \WeakReference::create($this);
1068-
$class = ($callable[0] instanceof Reference ? $this->findDefinition($callable[0]) : $callable[0])->getClass();
10691068
$initializer = static function () use ($containerRef, $callable, &$inlineServices) {
10701069
return $containerRef->get()->doResolveServices($callable[0], $inlineServices);
10711070
};
10721071

1073-
$proxy = eval('return '.LazyClosure::getCode('$initializer', $this->getReflectionClass($class), $callable[1], $id).';');
1072+
$proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';');
10741073
$this->shareService($definition, $proxy, $id, $inlineServices);
10751074

10761075
return $proxy;
10771076
}
10781077
}
10791078

1080-
if (true === $tryProxy && $definition->isLazy() && 'Closure' !== $class
1079+
if (true === $tryProxy && $definition->isLazy() && ['Closure', 'fromCallable'] !== $definition->getFactory()
10811080
&& !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator
10821081
) {
10831082
$containerRef = $this->containerRef ??= \WeakReference::create($this);

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

+40-41
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,8 @@ private function addNewInstance(Definition $definition, string $return = '', str
11621162
if ('current' === $callable && [0] === array_keys($definition->getArguments()) && \is_array($value) && [0] === array_keys($value)) {
11631163
return $return.$this->dumpValue($value[0]).$tail;
11641164
}
1165-
if (['Closure', 'fromCallable'] === $callable && [0] === array_keys($definition->getArguments())) {
1165+
1166+
if (['Closure', 'fromCallable'] === $callable) {
11661167
$callable = $definition->getArgument(0);
11671168
if ($callable instanceof ServiceClosureArgument) {
11681169
return $return.$this->dumpValue($callable).$tail;
@@ -1175,58 +1176,56 @@ private function addNewInstance(Definition $definition, string $return = '', str
11751176
}
11761177
}
11771178

1178-
if (\is_array($callable)) {
1179-
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
1180-
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
1181-
}
1182-
1183-
if (['...'] === $arguments && $definition->isLazy() && 'Closure' === ($definition->getClass() ?? 'Closure') && (
1184-
$callable[0] instanceof Reference
1185-
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
1186-
)) {
1187-
$class = ($callable[0] instanceof Reference ? $this->container->findDefinition($callable[0]) : $callable[0])->getClass();
1179+
if (\is_string($callable) && str_starts_with($callable, '@=')) {
1180+
return $return.sprintf('(($args = %s) ? (%s) : null)',
1181+
$this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
1182+
$this->getExpressionLanguage()->compile(substr($callable, 2), ['container' => 'container', 'args' => 'args'])
1183+
).$tail;
1184+
}
11881185

1189-
if (str_contains($initializer = $this->dumpValue($callable[0]), '$container')) {
1190-
$this->addContainerRef = true;
1191-
$initializer = sprintf('function () use ($containerRef) { $container = $containerRef; return %s; }', $initializer);
1192-
} else {
1193-
$initializer = 'fn () => '.$initializer;
1194-
}
1186+
if (!\is_array($callable)) {
1187+
return $return.sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail;
1188+
}
11951189

1196-
return $return.LazyClosure::getCode($initializer, $this->container->getReflectionClass($class), $callable[1], $id).$tail;
1197-
}
1190+
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
1191+
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
1192+
}
11981193

1199-
if ($callable[0] instanceof Reference
1200-
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
1201-
) {
1202-
return $return.sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1194+
if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && (
1195+
$callable[0] instanceof Reference
1196+
|| ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0]))
1197+
)) {
1198+
if (str_contains($initializer = $this->dumpValue($callable[0]), '$container')) {
1199+
$this->addContainerRef = true;
1200+
$initializer = sprintf('function () use ($containerRef) { $container = $containerRef->get(); return %s; }', $initializer);
1201+
} else {
1202+
$initializer = 'fn () => '.$initializer;
12031203
}
12041204

1205-
$class = $this->dumpValue($callable[0]);
1206-
// If the class is a string we can optimize away
1207-
if (str_starts_with($class, "'") && !str_contains($class, '$')) {
1208-
if ("''" === $class) {
1209-
throw new RuntimeException(sprintf('Cannot dump definition: "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline'));
1210-
}
1205+
return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail;
1206+
}
12111207

1212-
return $return.sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1213-
}
1208+
if ($callable[0] instanceof Reference
1209+
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
1210+
) {
1211+
return $return.sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1212+
}
12141213

1215-
if (str_starts_with($class, 'new ')) {
1216-
return $return.sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1214+
$class = $this->dumpValue($callable[0]);
1215+
// If the class is a string we can optimize away
1216+
if (str_starts_with($class, "'") && !str_contains($class, '$')) {
1217+
if ("''" === $class) {
1218+
throw new RuntimeException(sprintf('Cannot dump definition: "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline'));
12171219
}
12181220

1219-
return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
1221+
return $return.sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
12201222
}
12211223

1222-
if (\is_string($callable) && str_starts_with($callable, '@=')) {
1223-
return $return.sprintf('(($args = %s) ? (%s) : null)',
1224-
$this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
1225-
$this->getExpressionLanguage()->compile(substr($callable, 2), ['container' => 'container', 'args' => 'args'])
1226-
).$tail;
1224+
if (str_starts_with($class, 'new ')) {
1225+
return $return.sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
12271226
}
12281227

1229-
return $return.sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail;
1228+
return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
12301229
}
12311230

12321231
if (null === $class = $definition->getClass()) {
@@ -2344,7 +2343,7 @@ private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject,
23442343
{
23452344
$asGhostObject = false;
23462345

2347-
if ('Closure' === ($definition->getClass() ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null))) {
2346+
if (['Closure', 'fromCallable'] === $definition->getFactory()) {
23482347
return null;
23492348
}
23502349

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Loader\Configurator;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
16+
/**
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class FromCallableConfigurator extends AbstractServiceConfigurator
20+
{
21+
use Traits\AbstractTrait;
22+
use Traits\AutoconfigureTrait;
23+
use Traits\AutowireTrait;
24+
use Traits\BindTrait;
25+
use Traits\DecorateTrait;
26+
use Traits\DeprecateTrait;
27+
use Traits\LazyTrait;
28+
use Traits\PublicTrait;
29+
use Traits\ShareTrait;
30+
use Traits\TagTrait;
31+
32+
public const FACTORY = 'services';
33+
34+
private ServiceConfigurator $serviceConfigurator;
35+
36+
public function __construct(ServiceConfigurator $serviceConfigurator, Definition $definition)
37+
{
38+
$this->serviceConfigurator = $serviceConfigurator;
39+
40+
parent::__construct($serviceConfigurator->parent, $definition, $serviceConfigurator->id);
41+
}
42+
43+
public function __destruct()
44+
{
45+
$this->serviceConfigurator->__destruct();
46+
}
47+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ServiceConfigurator extends AbstractServiceConfigurator
3131
use Traits\DeprecateTrait;
3232
use Traits\FactoryTrait;
3333
use Traits\FileTrait;
34+
use Traits\FromCallableTrait;
3435
use Traits\LazyTrait;
3536
use Traits\ParentTrait;
3637
use Traits\PropertyTrait;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Loader\Configurator\Traits;
13+
14+
use Symfony\Component\DependencyInjection\ChildDefinition;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
use Symfony\Component\DependencyInjection\Loader\Configurator\FromCallableConfigurator;
17+
use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;
18+
use Symfony\Component\ExpressionLanguage\Expression;
19+
20+
trait FromCallableTrait
21+
{
22+
final public function fromCallable(string|array|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
23+
{
24+
if ($this->definition instanceof ChildDefinition) {
25+
throw new InvalidArgumentException('The configuration key "parent" is unsupported when using "fromCallable()".');
26+
}
27+
28+
foreach ([
29+
'synthetic' => 'isSynthetic',
30+
'factory' => 'getFactory',
31+
'file' => 'getFile',
32+
'arguments' => 'getArguments',
33+
'properties' => 'getProperties',
34+
'configurator' => 'getConfigurator',
35+
'calls' => 'getMethodCalls',
36+
] as $key => $method) {
37+
if ($this->definition->$method()) {
38+
throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported when using "fromCallable()".', $key));
39+
}
40+
}
41+
42+
$this->definition->setFactory(['Closure', 'fromCallable']);
43+
44+
if (\is_string($callable) && 1 === substr_count($callable, ':')) {
45+
$parts = explode(':', $callable);
46+
47+
throw new InvalidArgumentException(sprintf('Invalid callable "%s": the "service:method" notation is not available when using PHP-based DI configuration. Use "[service(\'%s\'), \'%s\']" instead.', $callable, $parts[0], $parts[1]));
48+
}
49+
50+
if ($callable instanceof Expression) {
51+
$callable = '@='.$callable;
52+
}
53+
54+
$this->definition->setArguments([static::processValue($callable, true)]);
55+
56+
if ('Closure' !== ($this->definition->getClass() ?? 'Closure')) {
57+
$this->definition->setLazy(true);
58+
} else {
59+
$this->definition->setClass('Closure');
60+
}
61+
62+
return new FromCallableConfigurator($this, $this->definition);
63+
}
64+
}

0 commit comments

Comments
 (0)