Skip to content

Commit f361fd0

Browse files
[DependencyInjection][ProxyManager] Use lazy-loading ghost object proxies when possible
1 parent 6cd0da0 commit f361fd0

26 files changed

+330
-165
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_class($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",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Bridge\ProxyManager\Internal;
13+
14+
use ProxyManager\Configuration;
15+
16+
/**
17+
* @internal
18+
*/
19+
trait LazyLoadingFactoryTrait
20+
{
21+
private readonly ProxyGenerator $generator;
22+
23+
public function __construct(Configuration $config, ProxyGenerator $generator)
24+
{
25+
parent::__construct($config);
26+
$this->generator = $generator;
27+
}
28+
29+
public function getGenerator(): ProxyGenerator
30+
{
31+
return $this->generator;
32+
}
33+
}

src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/LazyLoadingValueHolderGenerator.php renamed to src/Symfony/Bridge/ProxyManager/Internal/ProxyGenerator.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,35 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper;
12+
namespace Symfony\Bridge\ProxyManager\Internal;
1313

1414
use Laminas\Code\Generator\ClassGenerator;
15-
use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator as BaseGenerator;
15+
use ProxyManager\ProxyGenerator\LazyLoadingGhostGenerator;
16+
use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator;
17+
use ProxyManager\ProxyGenerator\ProxyGeneratorInterface;
1618
use Symfony\Component\DependencyInjection\Definition;
1719

1820
/**
1921
* @internal
2022
*/
21-
class LazyLoadingValueHolderGenerator extends BaseGenerator
23+
class ProxyGenerator implements ProxyGeneratorInterface
2224
{
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+
2335
/**
2436
* {@inheritdoc}
2537
*/
2638
public function generate(\ReflectionClass $originalClass, ClassGenerator $classGenerator, array $proxyOptions = []): void
2739
{
28-
parent::generate($originalClass, $classGenerator, $proxyOptions);
40+
$this->generator->generate($originalClass, $classGenerator, $proxyOptions);
2941

3042
foreach ($classGenerator->getMethods() as $method) {
3143
if (str_starts_with($originalClass->getFilename(), __FILE__)) {
@@ -40,14 +52,22 @@ public function generate(\ReflectionClass $originalClass, ClassGenerator $classG
4052
}
4153
}
4254

43-
public function getProxifiedClass(Definition $definition): ?string
55+
public function getProxifiedClass(Definition $definition, bool &$isGhostObject = null): ?string
4456
{
4557
if (!$definition->hasTag('proxy')) {
46-
return ($class = $definition->getClass()) && (class_exists($class) || interface_exists($class, false)) ? $class : null;
58+
if (!($class = $definition->getClass()) || !(class_exists($class) || interface_exists($class, false))) {
59+
return null;
60+
}
61+
62+
$class = new \ReflectionClass($class);
63+
$isGhostObject = !$class->isAbstract() && !$class->isInterface();
64+
65+
return $class->name;
4766
}
4867
if (!$definition->isLazy()) {
4968
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()));
5069
}
70+
$isGhostObject = false;
5171
$tags = $definition->getTag('proxy');
5272
if (!isset($tags[0]['interface'])) {
5373
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/LazyLoadingValueHolderFactory.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

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

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

1414
use ProxyManager\Configuration;
15+
use ProxyManager\Factory\LazyLoadingGhostFactory;
16+
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
1517
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
18+
use ProxyManager\Proxy\GhostObjectInterface;
1619
use ProxyManager\Proxy\LazyLoadingInterface;
20+
use Symfony\Bridge\ProxyManager\Internal\LazyLoadingFactoryTrait;
21+
use Symfony\Bridge\ProxyManager\Internal\ProxyGenerator;
1722
use Symfony\Component\DependencyInjection\ContainerInterface;
1823
use Symfony\Component\DependencyInjection\Definition;
1924
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
@@ -25,34 +30,55 @@
2530
*/
2631
class RuntimeInstantiator implements InstantiatorInterface
2732
{
28-
private LazyLoadingValueHolderFactory $factory;
33+
private Configuration $config;
34+
private ProxyGenerator $generator;
2935

3036
public function __construct()
3137
{
32-
$config = new Configuration();
33-
$config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
34-
35-
$this->factory = new LazyLoadingValueHolderFactory($config);
38+
$this->config = new Configuration();
39+
$this->config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
40+
$this->generator = new ProxyGenerator();
3641
}
3742

3843
/**
3944
* {@inheritdoc}
4045
*/
4146
public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object
4247
{
43-
return $this->factory->createProxy(
44-
$this->factory->getGenerator()->getProxifiedClass($definition),
45-
function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
46-
$wrappedInstance = $realInstantiator();
48+
$proxifiedClass = new \ReflectionClass($this->generator->getProxifiedClass($definition, $isGhostObject));
49+
$generator = $this->generator->asGhostObject($isGhostObject);
50+
51+
if ($isGhostObject) {
52+
$factory = new class($this->config, $generator) extends LazyLoadingGhostFactory {
53+
use LazyLoadingFactoryTrait;
54+
};
55+
56+
$initializer = static function (GhostObjectInterface $proxy, string $method, array $parameters, &$initializer, array $properties) use ($realInstantiator) {
57+
$instance = $realInstantiator($proxy);
58+
$initializer = null;
59+
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_class($instance)));
62+
}
4763

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();
4873
$proxy->setProxyInitializer(null);
4974

5075
return true;
51-
},
52-
[
53-
'fluentSafe' => $definition->hasTag('proxy'),
54-
'skipDestructor' => true,
55-
]
56-
);
76+
};
77+
}
78+
79+
return $factory->createProxy($proxifiedClass->name, $initializer, [
80+
'fluentSafe' => $definition->hasTag('proxy'),
81+
'skipDestructor' => true,
82+
]);
5783
}
5884
}

0 commit comments

Comments
 (0)