Skip to content

Commit 135fc98

Browse files
[VarExporter] Add trait to help implement lazy loading ghost objects
1 parent bbf25d6 commit 135fc98

15 files changed

+1004
-21
lines changed

src/Symfony/Component/VarExporter/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.2
55
---
66

7+
* Add `LazyGhostObjectTrait`
78
* Add `Hydrator::hydrate()`
89
* Preserve PHP references also when using `Hydrator::hydrate()` or `Instantiator::instantiate()`
910
* Add support for hydrating from native (array) casts

src/Symfony/Component/VarExporter/Hydrator.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,13 @@ public static function hydrate(object $instance, array $properties = [], array $
6262

6363
foreach ($properties as $name => &$value) {
6464
[$scope, $name] = $propertyScopes[$name] ?? [$class, $name];
65-
$scopedProperties[$scope][$name] = &$value;
65+
$scopedProperties[$scope ?? $propertyScopes[$name][0]][$name] = &$value;
6666
}
67-
unset($value);
6867
}
6968

70-
foreach ($scopedProperties as $class => $properties) {
69+
foreach ($scopedProperties as $scope => $properties) {
7170
if ($properties) {
72-
(InternalHydrator::$simpleHydrators[$class] ??= InternalHydrator::getSimpleHydrator($class))($properties, $instance);
71+
(InternalHydrator::$simpleHydrators[$scope] ??= InternalHydrator::getSimpleHydrator($scope))($properties, $instance);
7372
}
7473
}
7574

src/Symfony/Component/VarExporter/Instantiator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ public static function instantiate(string $class, array $properties = [], array
5454
$instance = unserialize('O:'.\strlen($class).':"'.$class.'":0:{}');
5555
}
5656

57-
return Hydrator::hydrate($instance, $properties, $scopedProperties);
57+
return $properties || $scopedProperties ? Hydrator::hydrate($instance, $properties, $scopedProperties) : $instance;
5858
}
5959
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
/**
15+
* @internal
16+
*/
17+
enum EmptyScope
18+
{
19+
}

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

+28-7
Original file line numberDiff line numberDiff line change
@@ -155,28 +155,35 @@ public static function getHydrator($class)
155155

156156
public static function getSimpleHydrator($class)
157157
{
158-
$baseHydrator = self::$simpleHydrators['stdClass'] ??= static function ($properties, $object) {
158+
$baseHydrator = self::$simpleHydrators['stdClass'] ??= (function ($properties, $object) {
159+
$readonly = (array) $this;
160+
159161
foreach ($properties as $name => &$value) {
160-
$object->$name = &$value;
162+
$object->$name = $value;
163+
164+
if (!($readonly[$name] ?? false)) {
165+
$object->$name = &$value;
166+
}
161167
}
162-
};
168+
})->bindTo(new \stdClass());
163169

164170
switch ($class) {
165171
case 'stdClass':
166172
return $baseHydrator;
167173

168174
case 'ErrorException':
169-
return $baseHydrator->bindTo(null, new class() extends \ErrorException {
175+
return $baseHydrator->bindTo(new \stdClass(), new class() extends \ErrorException {
170176
});
171177

172178
case 'TypeError':
173-
return $baseHydrator->bindTo(null, new class() extends \Error {
179+
return $baseHydrator->bindTo(new \stdClass(), new class() extends \Error {
174180
});
175181

176182
case 'SplObjectStorage':
177183
return static function ($properties, $object) {
178184
foreach ($properties as $name => &$value) {
179185
if ("\0" !== $name) {
186+
$object->$name = $value;
180187
$object->$name = &$value;
181188
continue;
182189
}
@@ -202,14 +209,22 @@ public static function getSimpleHydrator($class)
202209
if ("\0" === $name) {
203210
$constructor($object, $value);
204211
} else {
212+
$object->$name = $value;
205213
$object->$name = &$value;
206214
}
207215
}
208216
};
209217
}
210218

211219
if (!$classReflector->isInternal()) {
212-
return $baseHydrator->bindTo(null, $class);
220+
$readonly = new \stdClass();
221+
foreach ($classReflector->getProperties(\ReflectionProperty::IS_READONLY) as $propertyReflector) {
222+
if ($class === $propertyReflector->class) {
223+
$readonly->{$propertyReflector->name} = true;
224+
}
225+
}
226+
227+
return $baseHydrator->bindTo($readonly, $class);
213228
}
214229

215230
if ($classReflector->name !== $class) {
@@ -232,6 +247,7 @@ public static function getSimpleHydrator($class)
232247
if ($setValue = $propertySetters[$name] ?? null) {
233248
$setValue($object, $value);
234249
} else {
250+
$object->$name = $value;
235251
$object->$name = &$value;
236252
}
237253
}
@@ -255,7 +271,11 @@ public static function getPropertyScopes($class)
255271
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name];
256272
continue;
257273
}
258-
$propertyScopes[$name] = [$flags & \ReflectionProperty::IS_READONLY ? $property->class : $class, $name];
274+
if ($flags & \ReflectionProperty::IS_READONLY) {
275+
$class = $property->class;
276+
$propertyScopes["\0$class\0$name"] = [null, $name];
277+
}
278+
$propertyScopes[$name] = [$class, $name];
259279

260280
if (\ReflectionProperty::IS_PROTECTED & $flags) {
261281
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
@@ -269,6 +289,7 @@ public static function getPropertyScopes($class)
269289
if (!$property->isStatic()) {
270290
$name = $property->name;
271291
$propertyScopes["\0$class\0$name"] = [$class, $name];
292+
$propertyScopes[$name] ??= [$class, $name];
272293
}
273294
}
274295
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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\VarExporter\Hydrator;
15+
use Symfony\Component\VarExporter\Internal\Hydrator as InternalHydrator;
16+
17+
/**
18+
* @internal
19+
*/
20+
class LazyGhostObjectRegistry
21+
{
22+
public const STATUS_UNINITIALIZED = 1;
23+
public const STATUS_INITIALIZING = 2;
24+
public const STATUS_INITIALIZED = 3;
25+
26+
/**
27+
* @var array<int, \Closure>
28+
*/
29+
public static $initializers = [];
30+
31+
/**
32+
* @var array<int, array<string, mixed>>
33+
*/
34+
public static $knownProperties = [];
35+
36+
/**
37+
* @var array<class-string, array{0: array<string, mixed>, 1: list<\Closure>}>
38+
*/
39+
public static $propertiesDefaults = [];
40+
41+
/**
42+
* @var array<class-string, \ReflectionClass>
43+
*/
44+
public static $classReflectors = [];
45+
46+
/**
47+
* @var array<class-string, list<\Closure>>
48+
*/
49+
public static $classResetters = [];
50+
51+
/**
52+
* @var array<class-string, array<string, int>>
53+
*/
54+
public static $parentMethods = [];
55+
56+
public static function getParentMethods($class)
57+
{
58+
$parent = get_parent_class($class);
59+
60+
return [
61+
'__get' => $parent && method_exists($parent, '__get') ? ((new \ReflectionMethod($parent, '__get'))->returnsReference() ? 2 : 1) : 0,
62+
'__set' => $parent && method_exists($parent, '__set') ? 1 : 0,
63+
'__isset' => $parent && method_exists($parent, '__isset') ? 1 : 0,
64+
'__unset' => $parent && method_exists($parent, '__unset') ? 1 : 0,
65+
'__clone' => $parent && method_exists($parent, '__clone') ? 1 : 0,
66+
'__serialize' => $parent && method_exists($parent, '__serialize') ? 1 : 0,
67+
'__sleep' => $parent && method_exists($parent, '__sleep') ? 1 : 0,
68+
'__destruct' => $parent && method_exists($parent, '__destruct') ? 1 : 0,
69+
];
70+
}
71+
72+
public static function getClassResetters($class)
73+
{
74+
$classProperties = [];
75+
foreach (InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class) as $key => [$scope, $name]) {
76+
if ('lazyGhostObjectState' !== $name && null !== $scope) {
77+
$classProperties[$scope][$name] = $key;
78+
}
79+
}
80+
81+
$resetters = [];
82+
foreach ($classProperties as $scope => $properties) {
83+
$resetters[] = \Closure::bind(static function ($instance, $knownProperties) use ($properties) {
84+
foreach ($properties as $name => $key) {
85+
if (!\array_key_exists($key, $knownProperties)) {
86+
unset($instance->$name);
87+
continue;
88+
}
89+
$value = $knownProperties[$key];
90+
91+
if (null !== $value && $value !== $instance->$name ??= $value) {
92+
$instance->$name = $value;
93+
}
94+
}
95+
}, null, $scope);
96+
}
97+
98+
$resetters[] = static function ($instance, $knownProperties) {
99+
foreach ((array) $instance as $name => $value) {
100+
if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $knownProperties)) {
101+
unset($instance->$name);
102+
}
103+
}
104+
105+
foreach ($knownProperties as $name => $value) {
106+
if (null !== $value && "\0" !== ($name[0] ?? '') && $value !== $instance->$name ??= $value) {
107+
$instance->$name = $value;
108+
}
109+
}
110+
};
111+
112+
return $resetters;
113+
}
114+
115+
public static function initialize($instance, &$state)
116+
{
117+
$id = $state & ~3;
118+
$state = $id | self::STATUS_INITIALIZING;
119+
120+
$propertiesDefaults = self::$propertiesDefaults[\get_class($instance)];
121+
122+
foreach (self::$knownProperties[$id] ?? [] as $key => $value) {
123+
if (null !== $value) {
124+
$propertiesDefaults[$key] = $value;
125+
}
126+
}
127+
128+
Hydrator::hydrate($instance, $propertiesDefaults);
129+
self::$initializers[$id]($instance);
130+
131+
$state = $id | self::STATUS_INITIALIZED;
132+
133+
return true;
134+
}
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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;
13+
14+
interface LazyGhostObjectInterface
15+
{
16+
/**
17+
* @param \Closure(static):void $initializer
18+
* @param array<string, mixed> $knownProperties The values of the properties that are already known and won't trigger lazy-loading.
19+
* A null means to skip the corresponding property without changing its value.
20+
*/
21+
public static function createLazyGhostObject(\Closure $initializer, array $knownProperties = []): static;
22+
23+
public function initializeLazyGhostObject(): bool;
24+
25+
public function isLazyGhostObjectInitialized(): bool;
26+
27+
/**
28+
* @param array<string, mixed> $knownProperties The values of the properties that are already known and won't trigger lazy-loading.
29+
* A null means to skip the corresponding property without changing its value.
30+
*/
31+
public function resetLazyGhostObject(array $knownProperties = null): bool;
32+
}

0 commit comments

Comments
 (0)