Skip to content

Commit 798ba60

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

14 files changed

+994
-24
lines changed

src/Symfony/Component/VarExporter/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,14 @@ public static function hydrate(object $instance, array $properties = [], array $
6161
$propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class);
6262

6363
foreach ($properties as $name => &$value) {
64-
[$scope, $name] = $propertyScopes[$name] ?? [$class, $name];
65-
$scopedProperties[$scope][$name] = &$value;
64+
[$scope, $name, $readonlyScope] = $propertyScopes[$name] ?? [$class, $name, $class];
65+
$scopedProperties[$readonlyScope ?? $scope][$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

Lines changed: 1 addition & 1 deletion
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
}
Lines changed: 19 additions & 0 deletions
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

Lines changed: 27 additions & 9 deletions
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
}
@@ -252,10 +268,10 @@ public static function getPropertyScopes($class)
252268
$name = $property->name;
253269

254270
if (\ReflectionProperty::IS_PRIVATE & $flags) {
255-
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name];
271+
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null];
256272
continue;
257273
}
258-
$propertyScopes[$name] = [$flags & \ReflectionProperty::IS_READONLY ? $property->class : $class, $name];
274+
$propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $property->class : null];
259275

260276
if (\ReflectionProperty::IS_PROTECTED & $flags) {
261277
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
@@ -268,7 +284,9 @@ public static function getPropertyScopes($class)
268284
foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) {
269285
if (!$property->isStatic()) {
270286
$name = $property->name;
271-
$propertyScopes["\0$class\0$name"] = [$class, $name];
287+
$readonlyScope = $property->isReadOnly() ? $class : null;
288+
$propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope];
289+
$propertyScopes[$name] ??= [$class, $name, $readonlyScope];
272290
}
273291
}
274292
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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 as PublicHydrator;
15+
16+
/**
17+
* Stores the state of lazy ghost objects and caches related reflection information.
18+
*
19+
* As a micro-optimization, this classes uses no type-hints.
20+
*
21+
* @internal
22+
*/
23+
class LazyGhostObjectRegistry
24+
{
25+
/**
26+
* @var array<class-string, \ReflectionClass>
27+
*/
28+
public static $classReflectors = [];
29+
30+
/**
31+
* @var array<class-string, array<string, mixed>>
32+
*/
33+
public static $defaultProperties = [];
34+
35+
/**
36+
* @var array<int, \Closure>
37+
*/
38+
public static $initializers = [];
39+
40+
/**
41+
* @var array<class-string, list<\Closure>>
42+
*/
43+
public static $classResetters = [];
44+
45+
/**
46+
* @var array<int, array<string, array<string, true>>>
47+
*/
48+
public static $unsetProperties = [];
49+
50+
/**
51+
* @var array<int, int>
52+
*/
53+
public static $states = [];
54+
55+
/**
56+
* @var array<class-string, array{get: \Closure, set: \Closure, isset: \Closure, unset: \Closure}>
57+
*/
58+
public static $classAccessors = [];
59+
60+
/**
61+
* @var array<class-string, array{get: int, set: bool, isset: bool, unset: bool, clone: bool, serialize: bool, sleep: bool, destruct: bool}>
62+
*/
63+
public static $parentMethods = [];
64+
65+
public static function getClassResetters($class)
66+
{
67+
$classProperties = [];
68+
foreach (Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class) as $key => [$scope, $name, $readonlyScope]) {
69+
if ('lazyGhostObjectId' !== $name) {
70+
$classProperties[$readonlyScope ?? $scope][$name] = $key;
71+
}
72+
}
73+
74+
$resetters = [];
75+
foreach ($classProperties as $scope => $properties) {
76+
$resetters[] = \Closure::bind(static function ($instance) use ($properties) {
77+
foreach ($properties as $name => $key) {
78+
unset($instance->$name);
79+
}
80+
}, null, $scope);
81+
}
82+
83+
$resetters[] = static function ($instance) {
84+
foreach ((array) $instance as $name => $value) {
85+
if ("\0" !== ($name[0] ?? '')) {
86+
unset($instance->$name);
87+
}
88+
}
89+
};
90+
91+
return $resetters;
92+
}
93+
94+
public static function getClassAccessors($class)
95+
{
96+
return \Closure::bind(static function () {
97+
return [
98+
'get' => static function &($instance, $name, $readonly) {
99+
if (!$readonly) {
100+
return $instance->$name;
101+
}
102+
$value = $instance->$name;
103+
104+
return $value;
105+
},
106+
'set' => static function ($instance, $name, $value) {
107+
$instance->$name = $value;
108+
},
109+
'isset' => static function ($instance, $name) {
110+
return isset($instance->$name);
111+
},
112+
'unset' => static function ($instance, $name) {
113+
unset($instance->$name);
114+
},
115+
];
116+
}, null, $class)();
117+
}
118+
119+
public static function getParentMethods($class)
120+
{
121+
$parent = get_parent_class($class);
122+
123+
return [
124+
'get' => $parent && method_exists($parent, '__get') ? ((new \ReflectionMethod($parent, '__get'))->returnsReference() ? 2 : 1) : 0,
125+
'set' => $parent && method_exists($parent, '__set'),
126+
'isset' => $parent && method_exists($parent, '__isset'),
127+
'unset' => $parent && method_exists($parent, '__unset'),
128+
'clone' => $parent && method_exists($parent, '__clone'),
129+
'serialize' => $parent && method_exists($parent, '__serialize'),
130+
'sleep' => $parent && method_exists($parent, '__sleep'),
131+
'destruct' => $parent && method_exists($parent, '__destruct'),
132+
];
133+
}
134+
135+
public static function initialize($instance, $id, $propertyName, $propertyScope)
136+
{
137+
$class = \get_class($instance);
138+
139+
if (0 > $state = self::$states[$id] ??= 1 < (new \ReflectionFunction(self::$initializers[$id]))->getNumberOfRequiredParameters() ? 0 : 1) {
140+
return true;
141+
}
142+
143+
if ($state = -$state) {
144+
if ($defaultProperties = array_diff_key(self::$defaultProperties[$class], (array) $instance)) {
145+
PublicHydrator::hydrate($instance, $defaultProperties);
146+
}
147+
148+
self::$initializers[$id]($instance);
149+
150+
return true;
151+
}
152+
153+
$value = self::$initializers[$id](...[$instance, $propertyName, $propertyScope]);
154+
155+
$propertyScope ??= $class;
156+
$accessor = self::$classAccessors[$propertyScope] ??= self::getClassAccessors($propertyScope);
157+
158+
$accessor['set']($instance, $propertyName, $value);
159+
160+
return false;
161+
}
162+
}
Lines changed: 32 additions & 0 deletions
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|\Closure(static, string, ?string):mixed $initializer Initializes the instance passed as argument; when partial initialization
18+
* is desired the closure should take extra arguments $propertyName and
19+
* $propertyScope and should return the value of the corresponding property
20+
*/
21+
public static function createLazyGhostObject(\Closure $initializer): static;
22+
23+
/**
24+
* Forces initialization of a lazy ghost object.
25+
*/
26+
public function initializeLazyGhostObject(): void;
27+
28+
/**
29+
* @return bool Returns false when the object cannot be reset, ie when it's not a ghost object
30+
*/
31+
public function resetLazyGhostObject(): bool;
32+
}

0 commit comments

Comments
 (0)