Skip to content

Commit cf62f1a

Browse files
[VarExporter] Add trait to help implement lazy loading ghost objects
1 parent 473b5b2 commit cf62f1a

14 files changed

+1118
-10
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/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+
}
Lines changed: 126 additions & 0 deletions
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 declarations.
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+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 declarations.
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+
31+
/**
32+
* @var array<class-string|'*', array<string, true>>
33+
*/
34+
public $preInitUnsetProperties;
35+
36+
/**
37+
* @var array<string, true>
38+
*/
39+
public $preInitSetProperties = [];
40+
41+
/**
42+
* @var array<class-string|'*', array<string, true>>
43+
*/
44+
public $unsetProperties = [];
45+
46+
/**
47+
* One of self::STATUS_*.
48+
*
49+
* @var int
50+
*/
51+
public $status;
52+
53+
/**
54+
* @return bool Returns true when fully-initializing, false when partial-initializing
55+
*/
56+
public function initialize($instance, $propertyName, $propertyScope)
57+
{
58+
if (!$this->status) {
59+
$this->status = 1 < (new \ReflectionFunction($this->initializer))->getNumberOfRequiredParameters() ? self::STATUS_INITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL;
60+
$this->preInitUnsetProperties ??= $this->unsetProperties;
61+
}
62+
63+
if (self::STATUS_INITIALIZED_FULL === $this->status) {
64+
return true;
65+
}
66+
67+
if (self::STATUS_UNINITIALIZED_FULL === $this->status) {
68+
if ($defaultProperties = array_diff_key(GhostObjectRegistry::$defaultProperties[\get_class($instance)], (array) $instance)) {
69+
Hydrator::hydrate($instance, $defaultProperties);
70+
}
71+
72+
$this->status = self::STATUS_INITIALIZED_FULL;
73+
($this->initializer)($instance);
74+
75+
return true;
76+
}
77+
78+
$value = ($this->initializer)(...[$instance, $propertyName, $propertyScope]);
79+
80+
$propertyScope ??= \get_class($instance);
81+
$accessor = GhostObjectRegistry::$classAccessors[$propertyScope] ??= GhostObjectRegistry::getClassAccessors($propertyScope);
82+
83+
$accessor['set']($instance, $propertyName, $value);
84+
85+
return false;
86+
}
87+
}
Lines changed: 25 additions & 0 deletions
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)