Skip to content

Commit 9c48c76

Browse files
[DependencyInjection] Make it possible to cast closures to single-method interfaces
1 parent 163c570 commit 9c48c76

27 files changed

+453
-46
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
@@ -16,6 +16,8 @@ CHANGELOG
1616
* Allow extending the `Autowire` attribute
1717
* Add `#[Exclude]` to skip autoregistering a class
1818
* Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`
19+
* Make it possible to cast closures to single-method interfaces
20+
* Replace argument `&$asGhostObject` by `&$type` on LazyProxy's `DumperInterface`
1921
* Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
2022
* Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead
2123

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

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

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

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

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

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

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

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

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

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

10671067
if (isset($this->definitionVariables[$inlineDef])) {
10681068
$isSimpleInstance = false;
@@ -2323,14 +2323,14 @@ private function getClasses(Definition $definition, string $id): array
23232323
return $classes;
23242324
}
23252325

2326-
private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject, string $id): ?Definition
2326+
private function isProxyCandidate(Definition $definition, ?int &$type, string $id): ?Definition
23272327
{
2328-
$asGhostObject = false;
2328+
$type = DumperInterface::TYPE_PROXY;
23292329

23302330
if (!$definition->isLazy() || !$this->hasProxyDumper) {
23312331
return null;
23322332
}
23332333

2334-
return $this->getProxyDumper()->isProxyCandidate($definition, $asGhostObject, $id) ? $definition : null;
2334+
return $this->getProxyDumper()->isProxyCandidate($definition, $type, $id) ? $definition : null;
23352335
}
23362336
}

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);
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;
Lines changed: 64 additions & 0 deletions
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)