8000 [VarExporter] Leverage native lazy objects · symfony/symfony@aaf3d4a · GitHub
[go: up one dir, main page]

Skip to content

Commit aaf3d4a

Browse files
[VarExporter] Leverage native lazy objects
1 parent 273f2ee commit aaf3d4a

16 files changed

+908
-192
lines changed

UPGRADE-7.3.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,9 @@ VarDumper
196196

197197
* Deprecate `ResourceCaster::castCurl()`, `ResourceCaster::castGd()` and `ResourceCaster::castOpensslX509()`
198198
* Mark all casters as `@internal`
199+
200+
VarExporter
201+
-----------
202+
203+
* Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
204+
* Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 5 additions & 1 deletion
2044
Original file line numberDiff line numberDiff line change
@@ -2036,7 +2036,11 @@ public function testLazyAutowireAttributeWithIntersection()
20362036

20372037
$dumper = new PhpDumper($container);
20382038

2039-
$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute_with_intersection.php', $dumper->dump());
2039+
if (\PHP_VERSION_ID >= 80400) {
2040+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute_with_intersection.php', $dumper->dump());
2041+
} else {
2042+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/legacy_lazy_autowire_attribute_with_intersection.php', $dumper->dump());
2043+
}
2040
}
20412045

20422046
public function testCallableAdapterConsumer()

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,15 @@ protected static function get_Lazy_Foo_QFdMZVKService($container, $lazyLoad = tr
7474

7575
class objectProxy1fd6daa implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface
7676
{
77-
use \Symfony\Component\VarExporter\LazyProxyTrait;
77+
use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait {
78+
doCreateLazyProxy as public createLazyProxy;
79+
}
7880

7981
private const LAZY_OBJECT_PROPERTY_SCOPES = [];
8082

8183
public function initializeLazyObject(): \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface&\Symfony\Component\DependencyInjection\Tests\Compiler\IInterface
8284
{
83-
if ($state = $this->lazyObjectState ?? null) {
84-
return $state->realInstance ??= ($state->initializer)();
85-
}
86-
87-
return $this;
85+
return $this->lazyObjectState->realInstance;
8886
}
8987
}
9088

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
4+
use Symfony\Component\DependencyInjection\ContainerInterface;
5+
use Symfony\Component\DependencyInjection\Container;
6+
use Symfony\Component\DependencyInjection\Exception\LogicException;
7+
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
8+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
9+
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
10+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
11+
12+
/**
13+
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
14+
*/
15+
class ProjectServiceContainer extends Container
16+
{
17+
protected $parameters = [];
18+
19+
public function __construct()
20+
{
21+
$this->services = $this->privates = [];
22+
$this->methodMap = [
23+
'foo' => 'getFooService',
24+
];
25+
26+
$this->aliases = [];
27+
}
28+
29+
public function compile(): void
30+
{
31+
throw new LogicException('You cannot compile a dumped container that was already compiled.');
32+
}
33+
34+
public function isCompiled(): bool
35+
{
36+
return true;
37+
}
38+
39+
protected function createProxy($class, \Closure $factory)
40+
{
41+
return $factory();
42+
}
43+
44+
/**
45+
* Gets the public 'foo' shared autowired service.
46+
*
47+
* @return \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer
48+
*/
49+
protected static function getFooService($container)
50+
{
51+
$a = ($container->privates['.lazy.foo.qFdMZVK'] ?? self::get_Lazy_Foo_QFdMZVKService($container));
52+
53+
if (isset($container->services['foo'])) {
54+
return $container->services['foo'];
55+
}
56+
57+
return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer($a);
58+
}
59+
60+
/**
61+
* Gets the private '.lazy.foo.qFdMZVK' shared service.
62+
*
63+
* @return \object
64+
*/
65+
protected static function get_Lazy_Foo_QFdMZVKService($container, $lazyLoad = true)
66+
{
67+
if (true === $lazyLoad) {
68+
return $container->privates['.lazy.foo.qFdMZVK'] = $container->createProxy('objectProxy1fd6daa', static fn () => \objectProxy1fd6daa::createLazyProxy(static fn () => self::get_Lazy_Foo_QFdMZVKService($container, false)));
69+
}
70+
71+
return ($container->services['foo'] ?? self::getFooService($container));
72+
}
73+
}
74+
75+
class objectProxy1fd6daa implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface
76+
{
77+
use \Symfony\Component\VarExporter\LazyProxyTrait;
78+
79+
private const LAZY_OBJECT_PROPERTY_SCOPES = [];
80+
81+
public function initializeLazyObject(): \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface&\Symfony\Component\DependencyInjection\Tests\Compiler\IInterface
82+
{
83+
if ($state = $this->lazyObjectState ?? null) {
84+
return $state->realInstance ??= ($state->initializer)();
85+
}
86+
87+
return $this;
88+
}
89+
}
90+
91+
// Help opcache.preload discover always-needed symbols
92+
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
93+
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
94+
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);

src/Symfony/Component/VarExporter/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Leverage native lazy objects in `ProxyHelper::generateLazyProxy()` on PHP 8.4+
8+
* Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
9+
* Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead
10+
411
7.2
512
---
613

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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\Component\VarExporter\Internal;
13+
14+
use Symfony\Component\Serializer\Attribute\Ignore;
15+
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
16+
17+
/**
18+
* @internal
19+
*/
20+
trait LazyDecoratorTrait
21+
{
22+
#[Ignore]
23+
private readonly LazyObjectState $lazyObjectState;
24+
25+
/**
26+
* Creates a lazy-loading decorator.
27+
*
28+
* @param \Closure():object $initializer Returns the proxied object
29+
* @param static|null $instance
30+
*/
31+
private static function doCreateLazyProxy(\Closure $initializer, ?object $instance = null): static
32+
{
33+
$class = $instance ? $instance::class : static::class;
34+
35+
if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
36+
Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
37+
}
38+
39+
$r = Registry::$classReflectors[$class] ??= ($r = new \ReflectionClass($class))->hasProperty('lazyObjectState') ? $r : throw new \LogicException('Cannot create a lazy proxy for a non-decorator object.');
40+
$initializer = static function ($ghost) use ($initializer, $class) {
41+
foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
42+
$reset($ghost, []);
43+
}
44+
45+
$state = $ghost->lazyObjectState ??= new LazyObjectState();
46+
$state->realInstance = $initializer();
47+
$state->initializer = $initializer;
48+
};
49+
50+
if (!$instance) {
51+
return $r->newLazyGhost($initializer);
52+
}
53+
54+
$r->resetAsLazyGhost($instance, $initializer);
55+
56+
return $instance;
57+
}
58+
59+
public function __construct(...$args)
60+
{
61+
self::createLazyProxy(static fn () => new parent(...$args), $this);
62+
}
63+
64+
public function __destruct()
65+
{
66+
}
67+
68+
/**
69+
* Returns whether the object is initialized.
70+
*
71+
* @param bool $partial Whether partially initialized objects should be considered as initialized
72+
*/
73+
#[Ignore]
74+
public function isLazyObjectInitialized(bool $partial = false): bool
75+
{
76+
$r = Registry::$classReflectors[static::class] ??= new \ReflectionClass(static::class);
77+
78+
return !$r->isUninitializedLazyObject($this);
79+
}
80+
81+
/**
82+
* Forces initialization of a lazy object and returns it.
83+
*/
84+
public function initializeLazyObject(): parent
85+
{
86+
return $this->lazyObjectState->realInstance;
87+
}
88+
89+
/**
90+
* @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
91+
*/
92+
public function resetLazyObject(): bool
93+
{
94+
$r = Registry::$classReflectors[static::class] ??= new \ReflectionClass(static::class);
95+
96+
if ($r->isUninitializedLazyObject($this)) {
97+
return true;
98+
}
99+
100+
return isset($this->lazyObjectState->initializer) && self::createLazyProxy($this->lazyObjectState->initializer, $this);
101+
}
102+
103+
public function &__get($name): mixed
104+
{
105+
$instance = $this->lazyObjectState->realInstance;
106+
$class = $this::class;
107+
108+
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
109+
$notByRef = 0;
110+
111+
if ([, , , $access] = $propertyScopes[$name] ?? null) {
112+
$notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF || ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET;
113+
}
114+
115+
if ($notByRef || 2 !== ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['get'] ?: 2)) {
116+
$value = $instance->$name;
117+
118+
return $value;
119+
}
120+
121+
try {
122+
return $instance->$name;
123+
} catch (\Error $e) {
124+
if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
125+
throw $e;
126+
}
127+
128+
try {
129+
$instance->$name = [];
130+
131+
return $instance->$name;
132+
} catch (\Error) {
133+
if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) {
134+
throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious());
135+
}
136+
137+
throw $e;
138+
}
139+
}
140+
}
141+
142+
public function __set($name, $value): void
143+
{
144+
$this->lazyObjectState->realInstance->$name = $value;
145+
}
146+
147+
public function __isset($name): bool
148+
{
149+
return isset($this->lazyObjectState->realInstance->$name);
150+
}
151+
152+
public function __unset($name): void
153+
{
154+
unset($this->lazyObjectState->realInstance->$name);
155+
}
156+
157+
public function __serialize(): array
158+
{
159+
return [$this->lazyObjectState->realInstance];
160< 3E26 /code>+
}
161+
162+
public function __unserialize($data): void
163+
{
164+
$this->lazyObjectState = new LazyObjectState();
165+
$this->lazyObjectState->realInstance = $data[0];
166+
}
167+
168+
public function __clone(): void
169+
{
170+
$this->lazyObjectState = clone $this->lazyObjectState;
171+
}
172+
}

src/Symfony/Component/VarExporter/Internal/LazyObjectState.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ class LazyObjectState
3333
public int $status = self::STATUS_UNINITIALIZED_FULL;
3434

3535
public object $realInstance;
36+
public object $cloneInstance;
3637

3738
/**
3839
* @param array<string, true> $skippedProperties
3940
*/
4041
public function __construct(
41-
public \Closure $initializer,
42+
public ?\Closure $initializer = null,
4243
public array $skippedProperties = [],
4344
) {
4445
}
@@ -94,4 +95,17 @@ public function reset($instance): void
9495

9596
$this->status = self::STATUS_UNINITIALIZED_FULL;
9697
}
98+
99+
public function __clone()
100+
{
101+
if (isset($this->cloneInstance)) {
102+
try {
103+
$this->realInstance = $this->cloneInstance;
104+
} finally {
105+
unset($this->cloneInstance);
106+
}
107+
} elseif (isset($this->realInstance)) {
108+
$this->realInstance = clone $this->realInstance;
109+
}
110+
}
97111
}

src/Symfony/Component/VarExporter/LazyGhostTrait.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
use Symfony\Component\VarExporter\Internal\LazyObjectState;
1818
use Symfony\Component\VarExporter\Internal\LazyObjectTrait;
1919

20+
if (\PHP_VERSION_ID >= 80400) {
21+
trigger_deprecation('symfony/var-exporter', '7.3', 'The "%s" trait is deprecated, use native lazy objects instead.', LazyGhostTrait::class);
22+
}
23+
24+
/**
25+
* @deprecated since Symfony 7.3, use native lazy objects instead
26+
*/
2027
trait LazyGhostTrait
2128
{
2229
use LazyObjectTrait;

0 commit comments

Comments
 (0)
0