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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy