Skip to content

Commit 54f7ec5

Browse files
[DependencyInjection][VarExporter] Generate lazy proxies for non-ghostable lazy services out of the box
1 parent 04dfc1b commit 54f7ec5

18 files changed

+143
-236
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ CHANGELOG
44
6.2
55
---
66

7-
* Use lazy-loading ghost object proxies out of the box
7+
* Use lazy-loading ghost objects and virtual proxies out of the box
88
* Add argument `&$asGhostObject` to LazyProxy's `DumperInterface` to allow using ghost objects for lazy loading services
99
* Add `enum` env var processor
1010
* Add `shuffle` env var processor

Compiler/AutowirePass.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
use Symfony\Component\DependencyInjection\Definition;
2525
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
2626
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
27-
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
2827
use Symfony\Component\DependencyInjection\Reference;
2928
use Symfony\Component\DependencyInjection\TypedReference;
29+
use Symfony\Component\VarExporter\ProxyHelper;
3030
use Symfony\Contracts\Service\Attribute\SubscribedService;
3131

3232
/**
@@ -276,7 +276,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
276276
continue;
277277
}
278278

279-
$type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
279+
$type = ProxyHelper::exportType($parameter, true);
280280

281281
if ($checkAttributes) {
282282
foreach ($parameter->getAttributes() as $attribute) {
@@ -306,8 +306,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
306306
--$index;
307307
break;
308308
}
309-
$type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false);
310-
$type = $type ? sprintf('is type-hinted "%s"', ltrim($type, '\\')) : 'has no type-hint';
309+
$type = ProxyHelper::exportType($parameter);
310+
$type = $type ? sprintf('is type-hinted "%s"', preg_replace('/(^|[(|&])\\\\|^\?\\\\?/', '\1', $type)) : 'has no type-hint';
311311

312312
throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type));
313313
}

Compiler/ResolveBindingsPass.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
use Symfony\Component\DependencyInjection\Definition;
2020
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
2121
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
22-
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
2322
use Symfony\Component\DependencyInjection\Reference;
2423
use Symfony\Component\DependencyInjection\TypedReference;
24+
use Symfony\Component\VarExporter\ProxyHelper;
2525

2626
/**
2727
* @author Guilhem Niot <[email protected]>
@@ -176,10 +176,11 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
176176
continue;
177177
}
178178

179-
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter);
179+
$typeHint = ltrim(ProxyHelper::exportType($parameter) ?? '', '?');
180+
180181
$name = Target::parseName($parameter);
181182

182-
if ($typeHint && \array_key_exists($k = ltrim($typeHint, '\\').' $'.$name, $bindings)) {
183+
if ($typeHint && \array_key_exists($k = preg_replace('/(^|[(|&])\\\\/', '\1', $typeHint).' $'.$name, $bindings)) {
183184
$arguments[$key] = $this->getBindingValue($bindings[$k]);
184185

185186
continue;

Compiler/ResolveNamedArgumentsPass.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
1515
use Symfony\Component\DependencyInjection\Definition;
1616
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
17-
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
1817
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\VarExporter\ProxyHelper;
1919

2020
/**
2121
* Resolves named arguments to their corresponding numeric index.
@@ -87,7 +87,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
8787

8888
$typeFound = false;
8989
foreach ($parameters as $j => $p) {
90-
if (!\array_key_exists($j, $resolvedArguments) && ProxyHelper::getTypeHint($r, $p, true) === $key) {
90+
if (!\array_key_exists($j, $resolvedArguments) && ProxyHelper::exportType($p, true) === $key) {
9191
$resolvedArguments[$j] = $argument;
9292
$typeFound = true;
9393
}

ContainerBuilder.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
4747
use Symfony\Component\ExpressionLanguage\Expression;
4848
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
49-
use Symfony\Component\VarExporter\Hydrator;
5049

5150
/**
5251
* ContainerBuilder is a DI container that provides an API to easily describe services.
@@ -1037,10 +1036,6 @@ private function createService(Definition $definition, array &$inlineServices, b
10371036
if (null !== $factory) {
10381037
$service = $factory(...$arguments);
10391038

1040-
if (\is_object($tryProxy) && $service::class !== $parameterBag->resolveValue($definition->getClass())) {
1041-
throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1$s".', $definition->getClass(), get_debug_type($service)));
1042-
}
1043-
10441039
if (!$definition->isDeprecated() && \is_array($factory) && \is_string($factory[0])) {
10451040
$r = new \ReflectionClass($factory[0]);
10461041

@@ -1110,10 +1105,6 @@ private function createService(Definition $definition, array &$inlineServices, b
11101105
$callable($service);
11111106
}
11121107

1113-
if (\is_object($tryProxy) && $tryProxy !== $service) {
1114-
return Hydrator::hydrate($tryProxy, (array) $service);
1115-
}
1116-
11171108
return $service;
11181109
}
11191110

Dumper/PhpDumper.php

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -673,9 +673,6 @@ private function addServiceInstance(string $id, Definition $definition, bool $is
673673

674674
$return = '';
675675
if ($isSimpleInstance) {
676-
if ($asGhostObject && null !== $definition->getFactory()) {
677-
$instantiation .= '$this->hydrateProxy($lazyLoad, ';
678-
}
679676
$return = 'return ';
680677
} else {
681678
$instantiation .= ' = ';
@@ -893,9 +890,7 @@ protected function {$methodName}($lazyInitialization)
893890
$code .= sprintf(' %s ??= ', $factory);
894891

895892
if ($asFile) {
896-
$code .= "function () {\n";
897-
$code .= " return self::do(\$container);\n";
898-
$code .= " };\n\n";
893+
$code .= "fn () => self::do(\$container);\n\n";
899894
} else {
900895
$code .= sprintf("\$this->%s(...);\n\n", $methodName);
901896
}
@@ -1076,11 +1071,7 @@ private function addInlineService(string $id, Definition $definition, Definition
10761071
return $code;
10771072
}
10781073

1079-
if (!$asGhostObject) {
1080-
return $code."\n return \$instance;\n";
1081-
}
1082-
1083-
return $code."\n return \$this->hydrateProxy(\$lazyLoad, \$instance);\n";
1074+
return $code."\n return \$instance;\n";
10841075
}
10851076

10861077
private function addServices(array &$services = null): string
@@ -1326,19 +1317,6 @@ protected function createProxy(\$class, \Closure \$factory)
13261317
{$proxyLoader}return \$factory();
13271318
}
13281319
1329-
protected function hydrateProxy(\$proxy, \$instance)
1330-
{
1331-
if (\$proxy === \$instance) {
1332-
return \$proxy;
1333-
}
1334-
1335-
if (!\in_array(\get_class(\$instance), [\get_class(\$proxy), get_parent_class(\$proxy)], true)) {
1336-
throw new LogicException(sprintf('Lazy service of type "%s" cannot be hydrated because its factory returned an unexpected instance of "%s". Try adding the "proxy" tag to the corresponding service definition with attribute "interface" set to "%1\$s".', get_parent_class(\$proxy), get_debug_type(\$instance)));
1337-
}
1338-
1339-
return \Symfony\Component\VarExporter\Hydrator::hydrate(\$proxy, (array) \$instance);
1340-
}
1341-
13421320
EOF;
13431321
break;
13441322
}

LazyProxy/Instantiator/LazyServiceInstantiator.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111

1212
namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator;
1313

14-
use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator;
1514
use Symfony\Component\DependencyInjection\ContainerInterface;
1615
use Symfony\Component\DependencyInjection\Definition;
1716
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper;
18-
use Symfony\Component\VarExporter\LazyGhostObjectInterface;
19-
use Symfony\Component\VarExporter\LazyGhostObjectTrait;
17+
use Symfony\Component\VarExporter\LazyGhostTrait;
2018

2119
/**
2220
* @author Nicolas Grekas <[email protected]>
@@ -27,14 +25,10 @@ public function instantiateProxy(ContainerInterface $container, Definition $defi
2725
{
2826
$dumper = new LazyServiceDumper();
2927

30-
if ($dumper->useProxyManager($definition)) {
31-
return (new RuntimeInstantiator())->instantiateProxy($container, $definition, $id, $realInstantiator);
28+
if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $class), false)) {
29+
eval($dumper->getProxyCode($definition));
3230
}
3331

34-
if (!class_exists($proxyClass = $dumper->getProxyClass($definition), false)) {
35-
eval(sprintf('class %s extends %s implements %s { use %s; }', $proxyClass, $definition->getClass(), LazyGhostObjectInterface::class, LazyGhostObjectTrait::class));
36-
}
37-
38-
return $proxyClass::createLazyGhostObject($realInstantiator);
32+
return isset(class_uses($proxyClass)[LazyGhostTrait::class]) ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
3933
}
4034
}

LazyProxy/PhpDumper/LazyServiceDumper.php

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111

1212
namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper;
1313

14-
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
1514
use Symfony\Component\DependencyInjection\Definition;
1615
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
17-
use Symfony\Component\DependencyInjection\Exception\LogicException;
18-
use Symfony\Component\VarExporter\LazyGhostObjectInterface;
19-
use Symfony\Component\VarExporter\LazyGhostObjectTrait;
16+
use Symfony\Component\VarExporter\Exception\LogicException;
17+
use Symfony\Component\VarExporter\ProxyHelper;
2018

2119
/**
2220
* @author Nicolas Grekas <[email protected]>
@@ -48,29 +46,26 @@ public function isProxyCandidate(Definition $definition, bool &$asGhostObject =
4846
return false;
4947
}
5048

51-
$class = new \ReflectionClass($class);
52-
53-
if ($class->isFinal()) {
54-
throw new InvalidArgumentException(sprintf('Cannot make service of class "%s" lazy because the class is final.', $definition->getClass()));
49+
if ($definition->getFactory()) {
50+
return true;
5551
}
5652

57-
if ($asGhostObject = !$class->isAbstract() && !$class->isInterface() && (\stdClass::class === $class->name || !$class->isInternal())) {
58-
while ($class = $class->getParentClass()) {
59-
if (!$asGhostObject = \stdClass::class === $class->name || !$class->isInternal()) {
60-
break;
61-
}
53+
foreach ($definition->getMethodCalls() as $call) {
54+
if ($call[2] ?? false) {
55+
return true;
6256
}
6357
}
6458

59+
try {
60+
$asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
61+
} catch (LogicException) {
62+
}
63+
6564
return true;
6665
}
6766

6867
public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string
6968
{
70-
if ($dumper = $this->useProxyManager($definition)) {
71-
return $dumper->getProxyFactoryCode($definition, $id, $factoryCode);
72-
}
73-
7469
$instantiation = 'return';
7570

7671
if ($definition->isShared()) {
@@ -79,66 +74,75 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $
7974

8075
$proxyClass = $this->getProxyClass($definition);
8176

77+
if (!str_contains($factoryCode, '$proxy')) {
78+
return <<<EOF
79+
if (true === \$lazyLoad) {
80+
$instantiation \$this->createProxy('$proxyClass', fn () => \\$proxyClass::createLazyProxy(fn () => $factoryCode));
81+
}
82+
83+
84+
EOF;
85+
}
86+
8287
if (preg_match('/^\$this->\w++\(\$proxy\)$/', $factoryCode)) {
8388
$factoryCode = substr_replace($factoryCode, '(...)', -8);
8489
} else {
85-
$factoryCode = sprintf('function ($proxy) { return %s; }', $factoryCode);
90+
$factoryCode = sprintf('fn ($proxy) => %s', $factoryCode);
8691
}
8792

8893
return <<<EOF
89-
if (true === \$lazyLoad) {
90-
$instantiation \$this->createProxy('$proxyClass', function () {
91-
return \\$proxyClass::createLazyGhostObject($factoryCode);
92-
});
93-
}
94+
if (true === \$lazyLoad) {
95+
$instantiation \$this->createProxy('$proxyClass', fn () => \\$proxyClass::createLazyGhost($factoryCode));
96+
}
9497
9598
96-
EOF;
99+
EOF;
97100
}
98101

99102
public function getProxyCode(Definition $definition): string
100-
{
101-
if ($dumper = $this->useProxyManager($definition)) {
102-
return $dumper->getProxyCode($definition);
103-
}
104-
105-
$proxyClass = $this->getProxyClass($definition);
106-
107-
return sprintf(<<<EOF
108-
class %s extends \%s implements \%s
109-
{
110-
use \%s;
111-
}
112-
113-
EOF,
114-
$proxyClass,
115-
$definition->getClass(),
116-
LazyGhostObjectInterface::class,
117-
LazyGhostObjectTrait::class
118-
);
119-
}
120-
121-
public function getProxyClass(Definition $definition): string
122-
{
123-
$class = (new \ReflectionClass($definition->getClass()))->name;
124-
125-
return preg_replace('/^.*\\\\/', '', $class).'_'.substr(hash('sha256', $this->salt.'+'.$class), -7);
126-
}
127-
128-
public function useProxyManager(Definition $definition): ?ProxyDumper
129103
{
130104
if (!$this->isProxyCandidate($definition, $asGhostObject)) {
131105
throw new InvalidArgumentException(sprintf('Cannot instantiate lazy proxy for service of class "%s".', $definition->getClass()));
132106
}
107+
$proxyClass = $this->getProxyClass($definition, $class);
133108

134109
if ($asGhostObject) {
135-
return null;
110+
try {
111+
return 'class '.$proxyClass.ProxyHelper::generateLazyGhost($class);
112+
} catch (LogicException $e) {
113+
throw new InvalidArgumentException(sprintf('Cannot generate lazy ghost for service of class "%s" lazy.', $definition->getClass()), 0, $e);
114+
}
136115
}
137116

138-
if (!class_exists(ProxyDumper::class)) {
139-
throw new LogicException('You cannot use virtual proxies for lazy services as the ProxyManager bridge is not installed. Try running "composer require symfony/proxy-manager-bridge".');
117+
if ($definition->hasTag('proxy')) {
118+
$interfaces = [];
119+
foreach ($definition->getTag('proxy') as $tag) {
120+
if (!isset($tag['interface'])) {
121+
throw new InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on a "proxy" tag.', $definition->getClass()));
122+
}
123+
if (!interface_exists($tag['interface']) && !class_exists($tag['interface'], false)) {
124+
throw new InvalidArgumentException(sprintf('Invalid definition for service of class "%s": several "proxy" tags found but "%s" is not an interface.', $definition->getClass(), $tag['interface']));
125+
}
126+
$interfaces[] = new \ReflectionClass($tag['interface']);
127+
}
128+
} else {
129+
$interfaces = [$class];
140130
}
131+
if (1 === \count($interfaces) && !$interfaces[0]->isInterface()) {
132+
$class = array_pop($interfaces);
133+
}
134+
135+
try {
136+
return (\PHP_VERSION_ID >= 80200 && $class->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyProxy($class, $interfaces);
137+
} catch (LogicException $e) {
138+
throw new InvalidArgumentException(sprintf('Cannot generate lazy proxy for service of class "%s" lazy.', $definition->getClass()), 0, $e);
139+
}
140+
}
141+
142+
public function getProxyClass(Definition $definition, \ReflectionClass &$class = null): string
143+
{
144+
$class = new \ReflectionClass($definition->getClass());
141145

142-
return new ProxyDumper($this->salt);
146+
return preg_replace('/^.*\\\\/', '', $class->name).'_'.substr(hash('sha256', $this->salt.'+'.$class->name), -7);
143147
}
144148
}

LazyProxy/ProxyHelper.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\DependencyInjection\LazyProxy;
1313

14+
trigger_deprecation('symfony/dependency-injection', '6.2', 'The "%s" class is deprecated, use "%s" instead.', ProxyHelper::class, \Symfony\Component\VarExporter\ProxyHelper::class);
15+
1416
/**
1517
* @author Nicolas Grekas <[email protected]>
1618
*
17-
* @internal
19+
* @deprecated since Symfony 6.2, use VarExporter's ProxyHelper instead
1820
*/
1921
class ProxyHelper
2022
{

Tests/ContainerBuilderTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1696,7 +1696,8 @@ public function testLazyWither()
16961696

16971697
$wither = $container->get('wither');
16981698
$this->assertInstanceOf(Foo::class, $wither->foo);
1699-
$this->assertTrue($wither->resetLazyGhostObject());
1699+
$this->assertTrue($wither->resetLazyObject());
1700+
$this->assertInstanceOf(Wither::class, $wither->withFoo1($wither->foo));
17001701
}
17011702

17021703
public function testWitherWithStaticReturnType()

0 commit comments

Comments
 (0)