Skip to content

Commit 3a84d59

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

File tree

5 files changed

+124
-18
lines changed

5 files changed

+124
-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, LazyObjectRegistry::$defaultProperties[$class]))) {
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: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,23 @@ 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; it takes the
34+
* instance and its default properties as arguments.
3235
*
3336
* Properties should be indexed by their array-cast name, see
3437
* https://php.net/manual/language.types.array#language.types.array.casting
3538
*
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
39+
* @param (\Closure(static):void
40+
* |array<string, \Closure(static, string, ?string, mixed):mixed>
41+
* |array{"\0": \Closure(static, array<string, mixed>):array<string, mixed>}) $initializer
42+
* @param array<string, true>|null $skippedProperties An array indexed by the properties to skip, aka the ones
43+
* that the initializer doesn't set when its a closure
3944
*/
40-
public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = [], self $instance = null): static
45+
public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = null, self $instance = null): static
4146
{
47+
$onlyProperties = null === $skippedProperties && \is_array($initializer) ? $initializer : null;
48+
4249
if (self::class !== $class = $instance ? $instance::class : static::class) {
4350
$skippedProperties["\0".self::class."\0lazyObjectId"] = true;
4451
} elseif (\defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
@@ -48,8 +55,7 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp
4855
$instance ??= (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor();
4956
Registry::$defaultProperties[$class] ??= (array) $instance;
5057
$instance->lazyObjectId = $id = spl_object_id($instance);
51-
Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties);
52-
$onlyProperties = \is_array($initializer) ? $initializer : null;
58+
Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties ??= []);
5359

5460
foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
5561
$reset($instance, $skippedProperties, $onlyProperties);
@@ -60,8 +66,10 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp
6066

6167
/**
6268
* Returns whether the object is initialized.
69+
*
70+
* @param $partial Whether partially initialized objects should be considered as initialized
6371
*/
64-
public function isLazyObjectInitialized(): bool
72+
public function isLazyObjectInitialized(bool $partial = false): bool
6573
{
6674
if (!$state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) {
6775
return true;
@@ -73,6 +81,11 @@ public function isLazyObjectInitialized(): bool
7381

7482
$class = $this::class;
7583
$properties = (array) $this;
84+
85+
if ($partial) {
86+
return (bool) array_intersect_key($state->initializer, $properties);
87+
}
88+
7689
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
7790
foreach ($state->initializer as $key => $initializer) {
7891
if (!\array_key_exists($key, $properties) && isset($propertyScopes[$key])) {
@@ -100,16 +113,34 @@ public function initializeLazyObject(): static
100113
return $this;
101114
}
102115

116+
$values = isset($state->initializer["\0"]) ? null : [];
117+
103118
$class = $this::class;
104119
$properties = (array) $this;
105120
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
106121
foreach ($state->initializer as $key => $initializer) {
107122
if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) {
108123
continue;
109124
}
125+
$scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class);
110126

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

115146
return $this;

src/Symfony/Component/VarExporter/LazyObjectInterface.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ interface LazyObjectInterface
1515
{
1616
/**
1717
* Returns whether the object is initialized.
18+
*
19+
* @param $partial Whether partially initialized objects should be considered as initialized
1820
*/
19-
public function isLazyObjectInitialized(): bool;
21+
public function isLazyObjectInitialized(bool $partial = false): bool;
2022

2123
/**
2224
* Forces initialization of a lazy object and returns it.

src/Symfony/Component/VarExporter/LazyProxyTrait.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ public static function createLazyProxy(\Closure $initializer, self $instance = n
4747

4848
/**
4949
* Returns whether the object is initialized.
50+
*
51+
* @param $partial Whether partially initialized objects should be considered as initialized
5052
*/
51-
public function isLazyObjectInitialized(): bool
53+
public function isLazyObjectInitialized(bool $partial = false): bool
5254
{
5355
if (0 >= ($this->lazyObjectId ?? 0)) {
5456
return true;

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ 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));
251252
$this->assertSame(['public', "\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance));
252253
$this->assertSame(1, $counter);
253254

@@ -330,4 +331,56 @@ public function testReflectionPropertyGetValue()
330331

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

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