Skip to content

Commit 6971e57

Browse files
[VarExporter] Add trait to help implement lazy loading ghost objects
1 parent 46f7f27 commit 6971e57

15 files changed

+1063
-24
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

+4-5
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

+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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
* Stores the state of lazy ghost objects and caches related reflection information.
16+
*
17+
* As a micro-optimization, this class uses no type-hints.
18+
*
19+
* @internal
20+
*/
21+
class GhostObjectRegistry
22+
{
23+
/**
24+
* @var array<int, GhostObjectState>
25+
*/
26+
public static $states = [];
27+
28+
/**
29+
* @var array<class-string, \ReflectionClass>
30+
*/
31+
public static $classReflectors = [];
32+
33+
/**
34+
* @var array<class-string, array<string, mixed>>
35+
*/
36+
public static $defaultProperties = [];
37+
38+
/**
39+
* @var array<class-string, list<\Closure>>
40+
*/
41+
public static $classResetters = [];
42+
43+
/**
44+
* @var array<class-string, array{get: \Closure, set: \Closure, isset: \Closure, unset: \Closure}>
45+
*/
46+
public static $classAccessors = [];
47+
48+
/**
49+
* @var array<class-string, array{get: int, set: bool, isset: bool, unset: bool, clone: bool, serialize: bool, sleep: bool, destruct: bool}>
50+
*/
51+
public static $parentMethods = [];
52+
53+
public static function getClassResetters($class)
54+
{
55+
$classProperties = [];
56+
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
57+
58+
foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) {
59+
if ('lazyGhostObjectId' !== $name && null !== ($propertyScopes["\0$scope\0$name"] ?? $propertyScopes["\0*\0$name"] ?? $readonlyScope)) {
60+
$classProperties[$readonlyScope ?? $scope][$name] = $key;
61+
}
62+
}
63+
64+
$resetters = [];
65+
foreach ($classProperties as $scope => $properties) {
66+
$resetters[] = \Closure::bind(static function ($instance, $skippedProperties = []) use ($properties) {
67+
foreach ($properties as $name => $key) {
68+
if (!\array_key_exists($key, $skippedProperties)) {
69+
unset($instance->$name);
70+
}
71+
}
72+
}, null, $scope);
73+
}
74+
75+
$resetters[] = static function ($instance, $skippedProperties = []) {
76+
foreach ((array) $instance as $name => $value) {
77+
if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties)) {
78+
unset($instance->$name);
79+
}
80+
}
81+
};
82+
83+
return $resetters;
84+
}
85+
86+
public static function getClassAccessors($class)
87+
{
88+
return \Closure::bind(static function () {
89+
return [
90+
'get' => static function &($instance, $name, $readonly) {
91+
if (!$readonly) {
92+
return $instance->$name;
93+
}
94+
$value = $instance->$name;
95+
96+
return $value;
97+
},
98+
'set' => static function ($instance, $name, $value) {
99+
$instance->$name = $value;
100+
},
101+
'isset' => static function ($instance, $name) {
102+
return isset($instance->$name);
103+
},
104+
'unset' => static function ($instance, $name) {
105+
unset($instance->$name);
106+
},
107+
];
108+
}, null, $class)();
109+
}
110+
111+
public static function getParentMethods($class)
112+
{
113+
$parent = get_parent_class($class);
114+
115+
return [
116+
'get' => $parent && method_exists($parent, '__get') ? ((new \ReflectionMethod($parent, '__get'))->returnsReference() ? 2 : 1) : 0,
117+
'set' => $parent && method_exists($parent, '__set'),
118+
'isset' => $parent && method_exists($parent, '__isset'),
119+
'unset' => $parent && method_exists($parent, '__unset'),
120+
'clone' => $parent && method_exists($parent, '__clone'),
121+
'serialize' => $parent && method_exists($parent, '__serialize'),
122+
'sleep' => $parent && method_exists($parent, '__sleep'),
123+
'destruct' => $parent && method_exists($parent, '__destruct'),
124+
];
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
16+
/**
17+
* Keeps the state of lazy ghost objects.
18+
*
19+
* As a micro-optimization, this class uses no type-hints.
20+
*
21+
* @internal
22+
*/
23+
class GhostObjectState
24+
{
25+
public const STATUS_INITIALIZED_PARTIAL = 1;
26+
public const STATUS_UNINITIALIZED_FULL = 2;
27+
public const STATUS_INITIALIZED_FULL = 3;
28+
29+
public \Closure $initializer;
30+
public $preInitUnsetProperties;
31+
public $preInitSetProperties = [];
32+
public $unsetProperties = [];
33+
public $status;
34+
35+
public function initialize($instance, $propertyName, $propertyScope)
36+
{
37+
if (!$this->status) {
38+
$this->status = 1 < (new \ReflectionFunction($this->initializer))->getNumberOfRequiredParameters() ? self::STATUS_INITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL;
39+
$this->preInitUnsetProperties ??= $this->unsetProperties;
40+
}
41+
42+
if (self::STATUS_INITIALIZED_FULL === $this->status) {
43+
return true;
44+
}
45+
46+
if (self::STATUS_UNINITIALIZED_FULL === $this->status) {
47+
if ($defaultProperties = array_diff_key(GhostObjectRegistry::$defaultProperties[\get_class($instance)], (array) $instance)) {
48+
Hydrator::hydrate($instance, $defaultProperties);
49+
}
50+
51+
$this->status = self::STATUS_INITIALIZED_FULL;
52+
($this->initializer)($instance);
53+
54+
return true;
55+
}
56+
57+
$value = ($this->initializer)(...[$instance, $propertyName, $propertyScope]);
58+
59+
$propertyScope ??= \get_class($instance);
60+
$accessor = GhostObjectRegistry::$classAccessors[$propertyScope] ??= GhostObjectRegistry::getClassAccessors($propertyScope);
61+
62+
$accessor['set']($instance, $propertyName, $value);
63+
64+
return false;
65+
}
66+
}

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

+27-9
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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
* Forces initialization of a lazy ghost object.
18+
*/
19+
public function initializeLazyGhostObject(): void;
20+
21+
/**
22+
* @return bool Returns false when the object cannot be reset, ie when it's not a ghost object
23+
*/
24+
public function resetLazyGhostObject(): bool;
25+
}

0 commit comments

Comments
 (0)