Skip to content

Commit 613dd45

Browse files
[DependencyInjection][ProxyManager] Use lazy-loading ghost object proxies when possible
1 parent bbf25d6 commit 613dd45

File tree

13 files changed

+161
-78
lines changed

13 files changed

+161
-78
lines changed

src/Symfony/Bridge/Doctrine/ManagerRegistry.php

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,38 @@ protected function resetService($name): void
5050
if (!$manager instanceof LazyLoadingInterface) {
5151
throw new \LogicException('Resetting a non-lazy manager service is not supported. '.(interface_exists(LazyLoadingInterface::class) && class_exists(RuntimeInstantiator::class) ? sprintf('Declare the "%s" service as lazy.', $name) : 'Try running "composer require symfony/proxy-manager-bridge".'));
5252
}
53+
54+
$load = \Closure::bind(function () use ($name) {
55+
if (isset($this->aliases[$name])) {
56+
$name = $this->aliases[$name];
57+
}
58+
if (isset($this->fileMap[$name])) {
59+
return fn ($lazyLoad) => $this->load($this->fileMap[$name], $lazyLoad);
60+
}
61+
62+
return $this->{$this->methodMap[$name]}(...);
63+
}, $this->container, Container::class)();
64+
5365
if ($manager instanceof GhostObjectInterface) {
54-
throw new \LogicException('Resetting a lazy-ghost-object manager service is not supported.');
55-
}
56-
$manager->setProxyInitializer(\Closure::bind(
57-
function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) {
58-
if (isset($this->aliases[$name])) {
59-
$name = $this->aliases[$name];
60-
}
61-
if (isset($this->fileMap[$name])) {
62-
$wrappedInstance = $this->load($this->fileMap[$name], false);
63-
} else {
64-
$wrappedInstance = $this->{$this->methodMap[$name]}(false);
66+
$initializer = function (GhostObjectInterface $manager, string $method, array $parameters, &$initializer, array $properties) use ($load) {
67+
$instance = $load($manager);
68+
$initializer = null;
69+
70+
if ($instance !== $manager) {
71+
throw new \LogicException(sprintf('A lazy initializer should return the ghost object proxy it was given as argument, but an instance of "%s" was returned.', get_debug_type($instance)));
6572
}
6673

74+
return true;
75+
};
76+
} else {
77+
$initializer = function (&$wrappedInstance, LazyLoadingInterface $manager) use ($load) {
78+
$wrappedInstance = $load(false);
6779
$manager->setProxyInitializer(null);
6880

6981
return true;
70-
},
71-
$this->container,
72-
Container::class
73-
));
82+
};
83+
}
84+
85+
$manager->setProxyInitializer($initializer);
7486
}
7587
}

src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
namespace Symfony\Bridge\Doctrine\Tests;
1313

1414
use PHPUnit\Framework\TestCase;
15-
use ProxyManager\Proxy\LazyLoadingInterface;
16-
use ProxyManager\Proxy\ValueHolderInterface;
15+
use ProxyManager\Proxy\GhostObjectInterface;
1716
use Symfony\Bridge\Doctrine\ManagerRegistry;
1817
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
1918
use Symfony\Bridge\ProxyManager\Tests\LazyProxy\Dumper\PhpDumperTest;
@@ -38,13 +37,16 @@ public function testResetService()
3837
$registry->setTestContainer($container);
3938

4039
$foo = $container->get('foo');
41-
$foo->bar = 123;
42-
$this->assertTrue(isset($foo->bar));
4340

41+
$foo->bar = 234;
42+
$this->assertSame(234, $foo->bar);
4443
$registry->resetManager();
4544

45+
self::assertFalse($foo->isProxyInitialized());
46+
$foo->initializeProxy();
47+
4648
$this->assertSame($foo, $container->get('foo'));
47-
$this->assertObjectNotHasAttribute('bar', $foo);
49+
$this->assertSame(123, $foo->bar);
4850
}
4951

5052
/**
@@ -77,8 +79,7 @@ public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther()
7779
$service = $container->get('foo');
7880

7981
self::assertInstanceOf(\stdClass::class, $service);
80-
self::assertInstanceOf(LazyLoadingInterface::class, $service);
81-
self::assertInstanceOf(ValueHolderInterface::class, $service);
82+
self::assertInstanceOf(GhostObjectInterface::class, $service);
8283
self::assertFalse($service->isProxyInitialized());
8384

8485
$service->initializeProxy();
@@ -87,12 +88,7 @@ public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther()
8788
self::assertTrue($service->isProxyInitialized());
8889

8990
$registry->resetManager();
90-
$service->initializeProxy();
91-
92-
$wrappedValue = $service->getWrappedValueHolderValue();
93-
self::assertInstanceOf(\stdClass::class, $wrappedValue);
94-
self::assertNotInstanceOf(LazyLoadingInterface::class, $wrappedValue);
95-
self::assertNotInstanceOf(ValueHolderInterface::class, $wrappedValue);
91+
self::assertFalse($service->isProxyInitialized());
9692
}
9793

9894
private function dumpLazyServiceProjectAsFilesServiceContainer()
@@ -104,6 +100,7 @@ private function dumpLazyServiceProjectAsFilesServiceContainer()
104100
$container = new ContainerBuilder();
105101

106102
$container->register('foo', \stdClass::class)
103+
->setProperty('bar', 123)
107104
->setPublic(true)
108105
->setLazy(true);
109106
$container->compile();

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@
2828
"symfony/stopwatch": "^5.4|^6.0",
2929
"symfony/cache": "^5.4|^6.0",
3030
"symfony/config": "^5.4|^6.0",
31-
"symfony/dependency-injection": "^5.4|^6.0",
31+
"symfony/dependency-injection": "^6.2",
3232
"symfony/form": "^5.4.9|^6.0.9",
3333
"symfony/http-kernel": "^5.4|^6.0",
3434
"symfony/messenger": "^5.4|^6.0",
3535
"symfony/doctrine-messenger": "^5.4|^6.0",
3636
"symfony/property-access": "^5.4|^6.0",
3737
"symfony/property-info": "^5.4|^6.0",
38-
"symfony/proxy-manager-bridge": "^5.4|^6.0",
38+
"symfony/proxy-manager-bridge": "^6.2",
3939
"symfony/security-core": "^6.0",
4040
"symfony/expression-language": "^5.4|^6.0",
4141
"symfony/uid": "^5.4|^6.0",
@@ -55,7 +55,7 @@
5555
"doctrine/orm": "<2.7.4",
5656
"phpunit/phpunit": "<5.4.3",
5757
"symfony/cache": "<5.4",
58-
"symfony/dependency-injection": "<5.4",
58+
"symfony/dependency-injection": "<6.2",
5959
"symfony/form": "<5.4",
6060
"symfony/http-kernel": "<5.4",
6161
"symfony/messenger": "<5.4",

src/Symfony/Bridge/ProxyManager/Internal/ProxyGenerator.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bridge\ProxyManager\Internal;
1313

1414
use Laminas\Code\Generator\ClassGenerator;
15+
use ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator;
1516
use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator;
1617
use ProxyManager\ProxyGenerator\ProxyGeneratorInterface;
1718
use Symfony\Component\DependencyInjection\Definition;
@@ -21,12 +22,22 @@
2122
*/
2223
class ProxyGenerator implements ProxyGeneratorInterface
2324
{
25+
private readonly ProxyGeneratorInterface $generator;
26+
27+
public function asGhostObject(bool $asGhostObject): static
28+
{
29+
$clone = clone $this;
30+
$clone->generator = $asGhostObject ? new LazyLoadingGhostGenerator() : new LazyLoadingValueHolderGenerator();
31+
32+
return $clone;
33+
}
34+
2435
/**
2536
* {@inheritdoc}
2637
*/
2738
public function generate(\ReflectionClass $originalClass, ClassGenerator $classGenerator, array $proxyOptions = []): void
2839
{
29-
(new LazyLoadingValueHolderGenerator())->generate($originalClass, $classGenerator, $proxyOptions);
40+
$this->generator->generate($originalClass, $classGenerator, $proxyOptions);
3041

3142
foreach ($classGenerator->getMethods() as $method) {
3243
if (str_starts_with($originalClass->getFilename(), __FILE__)) {
@@ -41,18 +52,30 @@ public function generate(\ReflectionClass $originalClass, ClassGenerator $classG
4152
}
4253
}
4354

44-
public function getProxifiedClass(Definition $definition): ?string
55+
public function getProxifiedClass(Definition $definition, bool &$asGhostObject = null): ?string
4556
{
4657
if (!$definition->hasTag('proxy')) {
4758
if (!($class = $definition->getClass()) || !(class_exists($class) || interface_exists($class, false))) {
4859
return null;
4960
}
5061

51-
return (new \ReflectionClass($class))->name;
62+
$class = new \ReflectionClass($class);
63+
$name = $class->name;
64+
65+
if ($asGhostObject = !$class->isAbstract() && !$class->isInterface() && ('stdClass' === $class->name || !$class->isInternal())) {
66+
while ($class = $class->getParentClass()) {
67+
if (!$asGhostObject = 'stdClass' === $class->name || !$class->isInternal()) {
68+
break;
69+
}
70+
}
71+
}
72+
73+
return $name;
5274
}
5375
if (!$definition->isLazy()) {
5476
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": setting the "proxy" tag on a service requires it to be "lazy".', $definition->getClass()));
5577
}
78+
$asGhostObject = false;
5679
$tags = $definition->getTag('proxy');
5780
if (!isset($tags[0]['interface'])) {
5881
throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": the "interface" attribute is missing on the "proxy" tag.', $definition->getClass()));

src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Bridge\ProxyManager\LazyProxy\Instantiator;
1313

1414
use ProxyManager\Configuration;
15+
use ProxyManager\Factory\LazyLoadingGhostFactory;
1516
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
1617
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
18+
use ProxyManager\Proxy\GhostObjectInterface;
1719
use ProxyManager\Proxy\LazyLoadingInterface;
1820
use Symfony\Bridge\ProxyManager\Internal\LazyLoadingFactoryTrait;
1921
use Symfony\Bridge\ProxyManager\Internal\ProxyGenerator;
@@ -43,18 +45,36 @@ public function __construct()
4345
*/
4446
public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object
4547
{
46-
$proxifiedClass = new \ReflectionClass($this->generator->getProxifiedClass($definition));
48+
$proxifiedClass = new \ReflectionClass($this->generator->getProxifiedClass($definition, $asGhostObject));
49+
$generator = $this->generator->asGhostObject($asGhostObject);
4750

48-
$factory = new class($this->config, $this->generator) extends LazyLoadingValueHolderFactory {
49-
use LazyLoadingFactoryTrait;
50-
};
51+
if ($asGhostObject) {
52+
$factory = new class($this->config, $generator) extends LazyLoadingGhostFactory {
53+
use LazyLoadingFactoryTrait;
54+
};
5155

52-
$initializer = static function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
53-
$wrappedInstance = $realInstantiator();
54-
$proxy->setProxyInitializer(null);
56+
$initializer = static function (GhostObjectInterface $proxy, string $method, array $parameters, &$initializer, array $properties) use ($realInstantiator) {
57+
$instance = $realInstantiator($proxy);
58+
$initializer = null;
5559

56-
return true;
57-
};
60+
if ($instance !== $proxy) {
61+
throw new \LogicException(sprintf('A lazy initializer should return the ghost object proxy it was given as argument, but an instance of "%s" was returned.', get_debug_type($instance)));
62+
}
63+
64+
return true;
65+
};
66+
} else {
67+
$factory = new class($this->config, $generator) extends LazyLoadingValueHolderFactory {
68+
use LazyLoadingFactoryTrait;
69+
};
70+
71+
$initializer = static function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
72+
$wrappedInstance = $realInstantiator();
73+
$proxy->setProxyInitializer(null);
74+
75+
return true;
76+
};
77+
}
5878

5979
return $factory->createProxy($proxifiedClass->name, $initializer, [
6080
'fluentSafe' => $definition->hasTag('proxy'),

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ public function __construct(string $salt = '')
4242
*/
4343
public function isProxyCandidate(Definition $definition, bool &$asGhostObject = null): bool
4444
{
45-
$asGhostObject = false;
46-
47-
return ($definition->isLazy() || $definition->hasTag('proxy')) && $this->proxyGenerator->getProxifiedClass($definition);
45+
return ($definition->isLazy() || $definition->hasTag('proxy')) && $this->proxyGenerator->getProxifiedClass($definition, $asGhostObject);
4846
}
4947

5048
/**
@@ -58,9 +56,30 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $
5856
$instantiation .= sprintf(' $this->%s[%s] =', $definition->isPublic() && !$definition->isPrivate() ? 'services' : 'privates', var_export($id, true));
5957
}
6058

61-
$proxifiedClass = new \ReflectionClass($this->proxyGenerator->getProxifiedClass($definition));
59+
$proxifiedClass = new \ReflectionClass($this->proxyGenerator->getProxifiedClass($definition, $asGhostObject));
6260
$proxyClass = $this->getProxyClassName($proxifiedClass->name);
6361

62+
if ($asGhostObject) {
63+
return <<<EOF
64+
if (true === \$lazyLoad) {
65+
$instantiation \$this->createProxy('$proxyClass', function () {
66+
return \\$proxyClass::staticProxyConstructor(function (\ProxyManager\Proxy\GhostObjectInterface \$proxy, string \$method, array \$parameters, &\$initializer, array \$properties) {
67+
\$instance = $factoryCode;
68+
\$initializer = null;
69+
70+
if (\$instance !== \$proxy) {
71+
throw new \LogicException(sprintf('A lazy initializer should return the ghost object proxy it was given as argument, but an instance of "%s" was returned.', get_debug_type(\$instance)));
72+
}
73+
74+
return true;
75+
});
76+
});
77+
}
78+
79+
80+
EOF;
81+
}
82+
6483
return <<<EOF
6584
if (true === \$lazyLoad) {
6685
$instantiation \$this->createProxy('$proxyClass', function () {
@@ -96,10 +115,10 @@ private function getProxyClassName(string $class): string
96115

97116
private function generateProxyClass(Definition $definition): ClassGenerator
98117
{
99-
$class = $this->proxyGenerator->getProxifiedClass($definition);
118+
$class = $this->proxyGenerator->getProxifiedClass($definition, $asGhostObject);
100119
$generatedClass = new ClassGenerator($this->getProxyClassName($class));
101120

102-
$this->proxyGenerator->generate(new \ReflectionClass($class), $generatedClass, [
121+
$this->proxyGenerator->asGhostObject($asGhostObject)->generate(new \ReflectionClass($class), $generatedClass, [
103122
'fluentSafe' => $definition->hasTag('proxy'),
104123
'skipDestructor' => true,
105124
]);

src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
require_once __DIR__.'/Fixtures/includes/foo.php';
1515

1616
use PHPUnit\Framework\TestCase;
17-
use ProxyManager\Proxy\LazyLoadingInterface;
17+
use ProxyManager\Proxy\GhostObjectInterface;
1818
use ProxyManagerBridgeFooClass;
1919
use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator;
2020
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -38,23 +38,21 @@ public function testCreateProxyServiceWithRuntimeInstantiator()
3838

3939
$builder->compile();
4040

41-
/* @var $foo1 \ProxyManager\Proxy\LazyLoadingInterface|\ProxyManager\Proxy\ValueHolderInterface */
41+
/* @var $foo1 \ProxyManager\Proxy\GhostObjectInterface */
4242
$foo1 = $builder->get('foo1');
4343

4444
$foo1->__destruct();
4545
$this->assertSame(0, $foo1::$destructorCount);
4646

4747
$this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved on multiple subsequent calls');
4848
$this->assertInstanceOf(ProxyManagerBridgeFooClass::class, $foo1);
49-
$this->assertInstanceOf(LazyLoadingInterface::class, $foo1);
49+
$this->assertInstanceOf(GhostObjectInterface::class, $foo1);
5050
$this->assertFalse($foo1->isProxyInitialized());
5151

5252
$foo1->initializeProxy();
5353

5454
$this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved after initialization');
5555
$this->assertTrue($foo1->isProxyInitialized());
56-
$this->assertInstanceOf(ProxyManagerBridgeFooClass::class, $foo1->getWrappedValueHolderValue());
57-
$this->assertNotInstanceOf(LazyLoadingInterface::class, $foo1->getWrappedValueHolderValue());
5856

5957
$foo1->__destruct();
6058
$this->assertSame(1, $foo1::$destructorCount);

src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace Symfony\Bridge\ProxyManager\Tests\LazyProxy\Dumper;
1313

1414
use PHPUnit\Framework\TestCase;
15-
use ProxyManager\Proxy\LazyLoadingInterface;
15+
use ProxyManager\Proxy\GhostObjectInterface;
1616
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
1717
use Symfony\Component\DependencyInjection\ContainerBuilder;
1818
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
@@ -47,7 +47,7 @@ public function testDumpContainerWithProxyServiceWillShareProxies()
4747

4848
$proxy = $container->get('foo');
4949
$this->assertInstanceOf(\stdClass::class, $proxy);
50-
$this->assertInstanceOf(LazyLoadingInterface::class, $proxy);
50+
$this->assertInstanceOf(GhostObjectInterface::class, $proxy);
5151
$this->assertSame($proxy, $container->get('foo'));
5252

5353
$this->assertFalse($proxy->isProxyInitialized());
@@ -62,8 +62,10 @@ private function dumpLazyServiceProjectServiceContainer()
6262
{
6363
$container = new ContainerBuilder();
6464

65-
$container->register('foo', 'stdClass')->setPublic(true);
66-
$container->getDefinition('foo')->setLazy(true);
65+
$container->register('foo', 'stdClass')
66+
->setPublic(true)
67+
->setLazy(true)
68+
->setProperty('bar', 123);
6769
$container->compile();
6870

6971
$dumper = new PhpDumper($container);

0 commit comments

Comments
 (0)