Skip to content

Commit 99d80b0

Browse files
[DependencyInjection][ProxyManager] Use lazy-loading ghost object proxies when possible
1 parent 4b2a625 commit 99d80b0

25 files changed

+287
-139
lines changed

src/Symfony/Bridge/Doctrine/ManagerRegistry.php

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
namespace Symfony\Bridge\Doctrine;
1313

1414
use Doctrine\Persistence\AbstractManagerRegistry;
15+
use ProxyManager\Proxy\GhostObjectInterface;
1516
use ProxyManager\Proxy\LazyLoadingInterface;
1617
use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator;
1718
use Symfony\Component\DependencyInjection\Container;
19+
use Symfony\Component\VarExporter\Hydrator;
1820

1921
/**
2022
* References Doctrine connections and entity/document managers.
@@ -49,23 +51,38 @@ protected function resetService($name): void
4951
if (!$manager instanceof LazyLoadingInterface) {
5052
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".'));
5153
}
52-
$manager->setProxyInitializer(\Closure::bind(
53-
function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) {
54-
if (isset($this->aliases[$name])) {
55-
$name = $this->aliases[$name];
56-
}
57-
if (isset($this->fileMap[$name])) {
58-
$wrappedInstance = $this->load($this->fileMap[$name], false);
59-
} else {
60-
$wrappedInstance = $this->{$this->methodMap[$name]}(false);
54+
55+
$load = \Closure::bind(function () use ($name) {
56+
if (isset($this->aliases[$name])) {
57+
$name = $this->aliases[$name];
58+
}
59+
if (isset($this->fileMap[$name])) {
60+
return fn ($lazyLoad) => $this->load($this->fileMap[$name], $lazyLoad);
61+
}
62+
63+
return $this->{$this->methodMap[$name]}(...);
64+
}, $this->container, Container::class)();
65+
66+
if ($manager instanceof GhostObjectInterface) {
67+
$initializer = function (GhostObjectInterface $manager, string $method, array $parameters, &$initializer, array $properties) use ($load) {
68+
$instance = $load($manager);
69+
$initializer = null;
70+
71+
if ($instance !== $manager) {
72+
Hydrator::hydrate($manager, (array) $instance);
6173
}
6274

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

6582
return true;
66-
},
67-
$this->container,
68-
Container::class
69-
));
83+
};
84+
}
85+
86+
$manager->setProxyInitializer($initializer);
7087
}
7188
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ public function testResetService()
3838
$registry->setTestContainer($container);
3939

4040
$foo = $container->get('foo');
41-
$foo->bar = 123;
42-
$this->assertTrue(isset($foo->bar));
41+
$this->assertSame(123, $foo->bar);
4342

43+
$foo->bar = 234;
4444
$registry->resetManager();
4545

4646
$this->assertSame($foo, $container->get('foo'));
47-
$this->assertObjectNotHasAttribute('bar', $foo);
47+
$this->assertSame(123, $foo->bar);
4848
}
4949

5050
/**
@@ -104,6 +104,7 @@ private function dumpLazyServiceProjectAsFilesServiceContainer()
104104
$container = new ContainerBuilder();
105105

106106
$container->register('foo', \stdClass::class)
107+
->setProperty('bar', 123)
107108
->setPublic(true)
108109
->setLazy(true);
109110
$container->compile();
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()));
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 Symfony\Component\VarExporter\Hydrator;
15+
16+
/**
17+
* @internal
18+
*/
19+
class ProxyHydrator
20+
{
21+
public static function hydrate($proxy, $instance): bool
22+
{
23+
if (\get_parent_class($proxy) !== \get_class($instance)) {
24+
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_class($instance)));
25+
}
26+
27+
Hydrator::hydrate($proxy, (array) $instance);
28+
29+
return true;
30+
}
31+
}

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: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
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;
22+
use Symfony\Bridge\ProxyManager\Internal\ProxyHydrator;
1723
use Symfony\Component\DependencyInjection\ContainerInterface;
1824
use Symfony\Component\DependencyInjection\Definition;
1925
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
@@ -25,34 +31,52 @@
2531
*/
2632
class RuntimeInstantiator implements InstantiatorInterface
2733
{
28-
private LazyLoadingValueHolderFactory $factory;
34+
private Configuration $config;
35+
private ProxyGenerator $generator;
2936

3037
public function __construct()
3138
{
32-
$config = new Configuration();
33-
$config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
34-
35-
$this->factory = new LazyLoadingValueHolderFactory($config);
39+
$this->config = new Configuration();
40+
$this->config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
41+
$this->generator = new ProxyGenerator();
3642
}
3743

3844
/**
3945
* {@inheritdoc}
4046
*/
4147
public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object
4248
{
43-
return $this->factory->createProxy(
44-
$this->factory->getGenerator()->getProxifiedClass($definition),
45-
function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
46-
$wrappedInstance = $realInstantiator();
49+
$proxifiedClass = new \ReflectionClass($this->generator->getProxifiedClass($definition, $isGhostObject));
50+
$generator = $this->generator->asGhostObject($isGhostObject);
51+
52+
if ($isGhostObject) {
53+
$factory = new class($this->config, $generator) extends LazyLoadingGhostFactory {
54+
use LazyLoadingFactoryTrait;
55+
};
56+
57+
$initializer = static function (GhostObjectInterface $proxy, string $method, array $parameters, &$initializer, array $properties) use ($realInstantiator) {
58+
$instance = $realInstantiator($proxy);
59+
$initializer = null;
4760

61+
return $instance === $proxy || ProxyHydrator::hydrate($proxy, $instance);
62+
};
63+
} else {
64+
$factory = new class($this->config, $generator) extends LazyLoadingValueHolderFactory {
65+
use LazyLoadingFactoryTrait;
66+
};
67+
68+
$initializer = static function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) {
69+
$wrappedInstance = $realInstantiator();
4870
$proxy->setProxyInitializer(null);
4971

5072
return true;
51-
},
52-
[
53-
'fluentSafe' => $definition->hasTag('proxy'),
54-
'skipDestructor' => true,
55-
]
56-
);
73+
};
74+
}
75+
76+
77+
return $factory->createProxy($proxifiedClass->name, $initializer, [
78+
'fluentSafe' => $definition->hasTag('proxy'),
79+
'skipDestructor' => true,
80+
]);
5781
}
5882
}

0 commit comments

Comments
 (0)