Skip to content

Commit 3da7803

Browse files
[DependencyInjection] Make it possible to cast callables to single-method interfaces
1 parent 1f43acb commit 3da7803

29 files changed

+476
-49
lines changed

src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ public function __construct(string $salt = '')
4141
$this->classGenerator = new BaseGeneratorStrategy();
4242
}
4343

44-
public function isProxyCandidate(Definition $definition, bool &$asGhostObject = null, string $id = null): bool
44+
public function isProxyCandidate(Definition $definition, int &$type = null, string $id = null): bool
4545
{
46-
$asGhostObject = false;
46+
$type = 0; // DumperInterface::TYPE_PROXY
4747

4848
return ($definition->isLazy() || $definition->hasTag('proxy')) && $this->proxyGenerator->getProxifiedClass($definition);
4949
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ 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 to single-method interfaces
22+
* Replace argument `&$asGhostObject` by `&$type` on LazyProxy's `DumperInterface`
2123
* Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
2224
* Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead
2325

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,19 @@ 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+
if (\is_array($value)) {
295+
$value += [1 => '__invoke'];
296+
$callableId = $value[0] instanceof Definition ? $value[0]->getClass().$this->container->hash($value[0]) : $value[0];
297+
$callableId = '.callable.'.$callableId.'::'.$value[1];
298+
} else {
299+
$callableId = '.callable.'.$value;
300+
}
301+
302+
$this->container->register($callableId, 'Closure')
295303
->setFactory(['Closure', 'fromCallable'])
296-
->setArguments([$value + [1 => '__invoke']])
297-
->setLazy($attribute->lazy);
304+
->setArguments([$value])
305+
->setLazy($attribute->lazy || 'Closure' !== $type && 'callable' !== (string) $parameter->getType());
306+
$value = new Reference($callableId);
298307
} elseif ($attribute->lazy && ($value instanceof Reference ? !$this->container->has($value) || !$this->container->findDefinition($value)->isLazy() : null === $attribute->value && $type)) {
299308
$this->container->register('.lazy.'.$value ??= $getValue(), $type)
300309
->setFactory('current')

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -575,13 +575,13 @@ private function generateProxyClasses(): array
575575
$proxyDumper = $this->getProxyDumper();
576576
ksort($definitions);
577577
foreach ($definitions as $id => $definition) {
578-
if (!$definition = $this->isProxyCandidate($definition, $asGhostObject, $id)) {
578+
if (!$definition = $this->isProxyCandidate($definition, $proxyType, $id)) {
579579
continue;
580580
}
581-
if (isset($alreadyGenerated[$asGhostObject][$class = $definition->getClass()])) {
581+
if (isset($alreadyGenerated[$proxyType][$class = $definition->getClass()])) {
582582
continue;
583583
}
584-
$alreadyGenerated[$asGhostObject][$class] = true;
584+
$alreadyGenerated[$proxyType][$class] = true;
585585
// register class' reflector for resource tracking
586586
$this->container->getReflectionClass($class);
587587
if ("\n" === $proxyCode = "\n".$proxyDumper->getProxyCode($definition, $id)) {
@@ -678,8 +678,8 @@ private function addServiceInstance(string $id, Definition $definition, bool $is
678678
throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id));
679679
}
680680

681-
$asGhostObject = false;
682-
$isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id);
681+
$proxyType = DumperInterface::TYPE_PROXY;
682+
$isProxyCandidate = $this->isProxyCandidate($definition, $proxyType, $id);
683683
$instantiation = '';
684684

685685
$lastWitherIndex = null;
@@ -702,7 +702,7 @@ private function addServiceInstance(string $id, Definition $definition, bool $is
702702
$instantiation .= ' = ';
703703
}
704704

705-
return $this->addNewInstance($definition, ' '.$return.$instantiation, $id, $asGhostObject);
705+
return $this->addNewInstance($definition, ' '.$return.$instantiation, $id, DumperInterface::TYPE_GHOST === $proxyType);
706706
}
707707

708708
private function isTrivialInstance(Definition $definition): bool
@@ -907,8 +907,8 @@ protected static function {$methodName}(\$container$lazyInitialization)
907907
$factory = sprintf('$container->factories%s[%s]', $definition->isPublic() ? '' : "['service_container']", $this->doExport($id));
908908
}
909909

910-
$asGhostObject = false;
911-
if ($isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id)) {
910+
$proxyType = DumperInterface::TYPE_PROXY;
911+
if ($isProxyCandidate = $this->isProxyCandidate($definition, $proxyType, $id)) {
912912
$definition = $isProxyCandidate;
913913

914914
if (!$definition->isShared()) {
@@ -920,7 +920,7 @@ protected static function {$methodName}(\$container$lazyInitialization)
920920
$code .= sprintf("self::%s(...);\n\n", $methodName);
921921
}
922922
}
923-
$lazyLoad = $asGhostObject ? '$proxy' : 'false';
923+
$lazyLoad = DumperInterface::TYPE_GHOST === $proxyType ? '$proxy' : 'false';
924924
$this->addContainerRef = true;
925925

926926
$factoryCode = $asFile ? sprintf('self::do($containerRef->get(), %s)', $lazyLoad) : sprintf('self::%s($containerRef->get(), %s)', $methodName, $lazyLoad);
@@ -1062,8 +1062,8 @@ private function addInlineService(string $id, Definition $definition, Definition
10621062
return $code;
10631063
}
10641064

1065-
$asGhostObject = false;
1066-
$isProxyCandidate = $this->isProxyCandidate($inlineDef, $asGhostObject, $id);
1065+
$proxyType = DumperInterface::TYPE_PROXY;
1066+
$isProxyCandidate = $this->isProxyCandidate($inlineDef, $proxyType, $id);
10671067

10681068
if (isset($this->definitionVariables[$inlineDef])) {
10691069
$isSimpleInstance = false;
@@ -2340,9 +2340,9 @@ private function getClasses(Definition $definition, string $id): array
23402340
return $classes;
23412341
}
23422342

2343-
private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject, string $id): ?Definition
2343+
private function isProxyCandidate(Definition $definition, ?int &$type, string $id): ?Definition
23442344
{
2345-
$asGhostObject = false;
2345+
$type = DumperInterface::TYPE_PROXY;
23462346

23472347
if ('Closure' === ($definition->getClass() ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null))) {
23482348
return null;
@@ -2352,6 +2352,6 @@ private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject,
23522352
return null;
23532353
}
23542354

2355-
return $this->getProxyDumper()->isProxyCandidate($definition, $asGhostObject, $id) ? $definition : null;
2355+
return $this->getProxyDumper()->isProxyCandidate($definition, $type, $id) ? $definition : null;
23562356
}
23572357
}

src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/LazyServiceInstantiator.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\ContainerInterface;
1515
use Symfony\Component\DependencyInjection\Definition;
1616
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
17+
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
1718
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper;
1819

1920
/**
@@ -25,14 +26,14 @@ public function instantiateProxy(ContainerInterface $container, Definition $defi
2526
{
2627
$dumper = new LazyServiceDumper();
2728

28-
if (!$dumper->isProxyCandidate($definition, $asGhostObject, $id)) {
29+
if (!$dumper->isProxyCandidate($definition, $type, $id)) {
2930
throw new InvalidArgumentException(sprintf('Cannot instantiate lazy proxy for service "%s".', $id));
3031
}
3132

32-
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject, $class), false)) {
33+
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $type, $class), false)) {
3334
eval($dumper->getProxyCode($definition, $id));
3435
}
3536

36-
return $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
37+
return DumperInterface::TYPE_GHOST === $type ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
3738
}
3839
}

src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@
2020
*/
2121
interface DumperInterface
2222
{
23+
public const TYPE_PROXY = 0;
24+
public const TYPE_GHOST = 1;
25+
public const TYPE_CLOSURE = 2;
26+
2327
/**
2428
* Inspects whether the given definitions should produce proxy instantiation logic in the dumped container.
2529
*
26-
* @param bool|null &$asGhostObject Set to true after the call if the proxy is a ghost object
30+
* @param int|null &$type Set to the type of proxy after the call
2731
* @param string|null $id
2832
*/
29-
public function isProxyCandidate(Definition $definition/* , bool &$asGhostObject = null, string $id = null */): bool;
33+
public function isProxyCandidate(Definition $definition/* , int &$type = null, string $id = null */): bool;
3034

3135
/**
3236
* Generates the code to be used to instantiate a proxy in the dumped factory code.

src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/LazyServiceDumper.php

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ public function __construct(
2626
) {
2727
}
2828

29-
public function isProxyCandidate(Definition $definition, bool &$asGhostObject = null, string $id = null): bool
29+
public function isProxyCandidate(Definition $definition, int &$type = null, string $id = null): bool
3030
{
31-
$asGhostObject = false;
31+
$type = DumperInterface::TYPE_PROXY;
3232

3333
if ($definition->hasTag('proxy')) {
3434
if (!$definition->isLazy()) {
@@ -47,6 +47,10 @@ public function isProxyCandidate(Definition $definition, bool &$asGhostObject =
4747
}
4848

4949
if ($definition->getFactory()) {
50+
if (['Closure', 'fromCallable'] === $definition->getFactory()) {
51+
$type = DumperInterface::TYPE_CLOSURE;
52+
}
53+
5054
return true;
5155
}
5256

@@ -57,7 +61,7 @@ public function isProxyCandidate(Definition $definition, bool &$asGhostObject =
5761
}
5862

5963
try {
60-
$asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
64+
$type = ProxyHelper::generateLazyGhost(new \ReflectionClass($class)) ? DumperInterface::TYPE_GHOST : DumperInterface::TYPE_PROXY;
6165
} catch (LogicException) {
6266
}
6367

@@ -72,10 +76,14 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $
7276
$instantiation .= sprintf(' $container->%s[%s] =', $definition->isPublic() && !$definition->isPrivate() ? 'services' : 'privates', var_export($id, true));
7377
}
7478

75-
$asGhostObject = str_contains($factoryCode, '$proxy');
76-
$proxyClass = $this->getProxyClass($definition, $asGhostObject);
79+
$type = match (true) {
80+
str_contains($factoryCode, '$proxy') => DumperInterface::TYPE_GHOST,
81+
['Closure', 'fromCallable'] === $definition->getFactory() => DumperInterface::TYPE_CLOSURE,
82+
default => DumperInterface::TYPE_PROXY,
83+
};
84+
$proxyClass = $this->getProxyClass($definition, $type);
7785

78-
if (!$asGhostObject) {
86+
if (DumperInterface::TYPE_GHOST !== $type) {
7987
return <<<EOF
8088
if (true === \$lazyLoad) {
8189
$instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyProxy(static fn () => $factoryCode));
@@ -98,12 +106,12 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $
98106

99107
public function getProxyCode(Definition $definition, string $id = null): string
100108
{
101-
if (!$this->isProxyCandidate($definition, $asGhostObject, $id)) {
109+
if (!$this->isProxyCandidate($definition, $type, $id)) {
102110
throw new InvalidArgumentException(sprintf('Cannot instantiate lazy proxy for service "%s".', $id ?? $definition->getClass()));
103111
}
104-
$proxyClass = $this->getProxyClass($definition, $asGhostObject, $class);
112+
$proxyClass = $this->getProxyClass($definition, $type, $class);
105113

106-
if ($asGhostObject) {
114+
if (DumperInterface::TYPE_GHOST === $type) {
107115
try {
108116
return 'class '.$proxyClass.ProxyHelper::generateLazyGhost($class);
109117
} catch (LogicException $e) {
@@ -132,19 +140,31 @@ public function getProxyCode(Definition $definition, string $id = null): string
132140
$class = null;
133141
}
134142

143+
if (DumperInterface::TYPE_CLOSURE === $type) {
144+
if (!($class xor 1 === \count($interfaces))) {
145+
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": the proxy shoud be based on exactly one class or interface.', $id ?? $definition->getClass()));
146+
}
147+
if (1 !== \count(($class ?? $interfaces[0])->getMethods(\ReflectionMethod::IS_PUBLIC))) {
148+
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": the proxied interface should have exactly one public method.', $id ?? $definition->getClass()));
149+
}
150+
}
151+
135152
try {
136-
return (\PHP_VERSION_ID >= 80200 && $class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyProxy($class, $interfaces);
153+
return (\PHP_VERSION_ID >= 80200 && $class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyProxy($class, $interfaces, DumperInterface::TYPE_CLOSURE === $type ? ProxyHelper::FLAG_FOR_CLOSURE : 0);
137154
} catch (LogicException $e) {
138155
throw new InvalidArgumentException(sprintf('Cannot generate lazy proxy for service "%s".', $id ?? $definition->getClass()), 0, $e);
139156
}
140157
}
141158

142-
public function getProxyClass(Definition $definition, bool $asGhostObject, \ReflectionClass &$class = null): string
159+
public function getProxyClass(Definition $definition, int $type, \ReflectionClass &$class = null): string
143160
{
144161
$class = new \ReflectionClass($definition->getClass());
145162

146163
return preg_replace('/^.*\\\\/', '', $class->name)
147-
.($asGhostObject ? 'Ghost' : 'Proxy')
148-
.ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name), -7));
164+
.match ($type) {
165+
DumperInterface::TYPE_PROXY => 'Proxy',
166+
DumperInterface::TYPE_GHOST => 'Ghost',
167+
DumperInterface::TYPE_CLOSURE => 'Closure',
168+
}.ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name), -7));
149169
}
150170
}

src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
*/
2323
class NullDumper implements DumperInterface
2424
{
25-
public function isProxyCandidate(Definition $definition, bool &$asGhostObject = null, string $id = null): bool
25+
public function isProxyCandidate(Definition $definition, int &$type = null, string $id = null): bool
2626
{
27-
return $asGhostObject = false;
27+
$type = DumperInterface::TYPE_PROXY;
28+
29+
return false;
2830
}
2931

3032
public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string
Lines changed: 47 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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;

0 commit comments

Comments
 (0)