Skip to content

Commit c5f79f6

Browse files
[VarExporter] Improve partial-initialization API for ghost objects
1 parent 19940ff commit c5f79f6

File tree

5 files changed

+126
-18
lines changed

5 files changed

+126
-18
lines changed

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,34 @@ public function initialize($instance, $propertyName, $propertyScope)
5555
$propertyScopes = Hydrator::$propertyScopes[$class];
5656
$propertyScopes[$k = "\0$propertyScope\0$propertyName"] ?? $propertyScopes[$k = "\0*\0$propertyName"] ?? $k = $propertyName;
5757

58-
if (!$initializer = $this->initializer[$k] ?? null) {
59-
return self::STATUS_UNINITIALIZED_PARTIAL;
60-
}
58+
if ($initializer = $this->initializer[$k] ?? null) {
59+
$value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]);
60+
$accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope);
61+
$accessor['set']($instance, $propertyName, $value);
6162

62-
$value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]);
63+
return $this->status = self::STATUS_INITIALIZED_PARTIAL;
64+
}
6365

64-
$accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope);
65-
$accessor['set']($instance, $propertyName, $value);
66+
$status = self::STATUS_UNINITIALIZED_PARTIAL;
67+
68+
if ($initializer = $this->initializer["\0"] ?? null) {
69+
if (!\is_array($values = $initializer($instance))) {
70+
throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values)));
71+
}
72+
$properties = (array) $instance;
73+
foreach ($values as $key => $value) {
74+
if ($k === $key) {
75+
$status = self::STATUS_INITIALIZED_PARTIAL;
76+
}
77+
if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) {
78+
$scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class);
79+
$accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope);
80+
$accessor['set']($instance, $name, $value);
81+
}
82+
}
83+
}
6684

67-
return $this->status = self::STATUS_INITIALIZED_PARTIAL;
85+
return $status;
6886
}
6987

7088
$this->status = self::STATUS_INITIALIZED_FULL;

src/Symfony/Component/VarExporter/LazyGhostTrait.php

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,20 @@ trait LazyGhostTrait
2929
* properties and closures should accept 4 arguments: the instance to
3030
* initialize, the property to initialize, its write-scope, and its default
3131
* value. Each closure should return the value of the corresponding property.
32+
* The special "\0" key can be used to define a closure that returns all
33+
* properties at once when full-initialization is needed.
3234
*
3335
* Properties should be indexed by their array-cast name, see
3436
* https://php.net/manual/language.types.array#language.types.array.casting
3537
*
36-
* @param \Closure(static):void|array<string, \Closure(static, string, ?string, mixed):mixed> $initializer
37-
* @param array<string, true> $skippedProperties An array indexed by the properties to skip, aka the ones
38-
* that the initializer doesn't set when its a closure
38+
* @param \Closure(static):void|array<string, \Closure(static, string, ?string, mixed):mixed>|array{"\0": \Closure(static):array} $initializer
39+
* @param array<string, true>|null $skippedProperties An array indexed by the properties to skip, aka the ones
40+
* that the initializer doesn't set when its a closure
3941
*/
40-
public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = [], self $instance = null): static
42+
public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = null, self $instance = null): static
4143
{
44+
$onlyProperties = null === $skippedProperties && \is_array($initializer) ? $initializer : null;
45+
4246
if (self::class !== $class = $instance ? $instance::class : static::class) {
4347
$skippedProperties["\0".self::class."\0lazyObjectId"] = true;
4448
} elseif (\defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
@@ -48,8 +52,7 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp
4852
$instance ??= (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor();
4953
Registry::$defaultProperties[$class] ??= (array) $instance;
5054
$instance->lazyObjectId = $id = spl_object_id($instance);
51-
Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties);
52-
$onlyProperties = \is_array($initializer) ? $initializer : null;
55+
Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties ??= []);
5356

5457
foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
5558
$reset($instance, $skippedProperties, $onlyProperties);
@@ -61,7 +64,7 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp
6164
/**
6265
* Returns whether the object is initialized.
6366
*/
64-
public function isLazyObjectInitialized(): bool
67+
public function isLazyObjectInitialized(bool|string $partial = false): bool
6568
{
6669
if (!$state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) {
6770
return true;
@@ -73,6 +76,21 @@ public function isLazyObjectInitialized(): bool
7376

7477
$class = $this::class;
7578
$properties = (array) $this;
79+
80+
if (\is_string($partial)) {
81+
return \array_key_exists($partial, $properties);
82+
}
83+
84+
if ($partial) {
85+
foreach ($state->initializer as $key => $initializer) {
86+
if (\array_key_exists($key, $properties)) {
87+
return true;
88+
}
89+
}
90+
91+
return false;
92+
}
93+
7694
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
7795
foreach ($state->initializer as $key => $initializer) {
7896
if (!\array_key_exists($key, $properties) && isset($propertyScopes[$key])) {
@@ -100,16 +118,34 @@ public function initializeLazyObject(): static
100118
return $this;
101119
}
102120

121+
$values = isset($state->initializer["\0"]) ? null : [];
122+
103123
$class = $this::class;
104124
$properties = (array) $this;
105125
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
106126
foreach ($state->initializer as $key => $initializer) {
107127
if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) {
108128
continue;
109129
}
130+
$scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class);
110131

111-
$state->initialize($this, $name, $readonlyScope ?? ('*' !== $scope ? $scope : null));
112-
$properties = (array) $this;
132+
if (null === $values) {
133+
if (!\is_array($values = ($state->initializer["\0"])($this))) {
134+
throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values)));
135+
}
136+
137+
if (\array_key_exists($key, $properties = (array) $this)) {
138+
continue;
139+
}
140+
}
141+
142+
if (\array_key_exists($key, $values)) {
143+
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
144+
$accessor['set']($this, $name, $properties[$key] = $values[$key]);
145+
} else {
146+
$state->initialize($this, $name, $scope);
147+
$properties = (array) $this;
148+
}
113149
}
114150

115151
return $this;

src/Symfony/Component/VarExporter/LazyObjectInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface LazyObjectInterface
1616
/**
1717
* Returns whether the object is initialized.
1818
*/
19-
public function isLazyObjectInitialized(): bool;
19+
public function isLazyObjectInitialized(bool|string $partial = false): bool;
2020

2121
/**
2222
* Forces initialization of a lazy object and returns it.

src/Symfony/Component/VarExporter/LazyProxyTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static function createLazyProxy(\Closure $initializer, self $instance = n
4848
/**
4949
* Returns whether the object is initialized.
5050
*/
51-
public function isLazyObjectInitialized(): bool
51+
public function isLazyObjectInitialized(bool|string $partial = false): bool
5252
{
5353
if (0 >= ($this->lazyObjectId ?? 0)) {
5454
return true;

src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ public function testPartialInitialization()
248248
$this->assertFalse($instance->isLazyObjectInitialized());
249249
$this->assertSame(123, $instance->public);
250250
$this->assertFalse($instance->isLazyObjectInitialized());
251+
$this->assertTrue($instance->isLazyObjectInitialized(true));
252+
$this->assertTrue($instance->isLazyObjectInitialized('public'));
253+
$this->assertFalse($instance->isLazyObjectInitialized('publicReadonly'));
251254
$this->assertSame(['public', "\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance));
252255
$this->assertSame(1, $counter);
253256

@@ -330,4 +333,55 @@ public function testReflectionPropertyGetValue()
330333

331334
$this->assertSame(-3, $r->getValue($obj));
332335
}
336+
337+
public function testFullPartialInitialization()
338+
{
339+
$counter = 0;
340+
$initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) {
341+
return 234;
342+
};
343+
$instance = ChildTestClass::createLazyGhost([
344+
'public' => $initializer,
345+
'publicReadonly' => $initializer,
346+
"\0*\0protected" => $initializer,
347+
"\0" => function ($obj) use (&$instance, &$counter) {
348+
$counter += 1000;
349+
$this->assertSame($instance, $obj);
350+
351+
return [
352+
'public' => 345,
353+
'publicReadonly' => 456,
354+
"\0*\0protected" => 567,
355+
];
356+
},
357+
]);
358+
359+
$this->assertSame($instance, $instance->initializeLazyObject());
360+
$this->assertSame(345, $instance->public);
361+
$this->assertSame(456, $instance->publicReadonly);
362+
$this->assertSame(567, ((array) $instance)["\0*\0protected"]);
363+
$this->assertSame(1000, $counter);
364+
}
365+
366+
public function testPartialInitializationFallback()
367+
{
368+
$counter = 0;
369+
$instance = ChildTestClass::createLazyGhost([
370+
"\0" => function ($obj) use (&$instance, &$counter) {
371+
$counter += 1000;
372+
$this->assertSame($instance, $obj);
373+
374+
return [
375+
'public' => 345,
376+
'publicReadonly' => 456,
377+
"\0*\0protected" => 567,
378+
];
379+
},
380+
], []);
381+
382+
$this->assertSame(345, $instance->public);
383+
$this->assertSame(456, $instance->publicReadonly);
384+
$this->assertSame(567, ((array) $instance)["\0*\0protected"]);
385+
$this->assertSame(1000, $counter);
386+
}
333387
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy