8000 bug #48312 [VarExporter] Improve partial-initialization API for ghost… · symfony/symfony@894adcb · GitHub
[go: up one dir, main page]

Skip to content

Commit 894adcb

Browse files
committed
bug #48312 [VarExporter] Improve partial-initialization API for ghost objects (nicolas-grekas)
This PR was merged into the 6.2 branch. Discussion ---------- [VarExporter] Improve partial-initialization API for ghost objects | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Discussed with `@alcaeus` at SymfonyCon, this improves support for partial-initialization of lazy ghost objects. - `isLazyObjectInitialized(bool|string $partial = false): bool` allows checking if an object is partially-initialized, or if a specific property is initialized. - a partial initializer set to the special `"\0"` key can be used to initialize all/many properties at once. Targeting 6.2 because this would be a BC break in 6.3, also because this improves a new unreleased feature. Commits ------- cd5bf0c [VarExporter] Improve partial-initialization API for ghost objects
2 parents 349f2b2 + cd5bf0c commit 894adcb

File tree

5 files changed

+157
-18
lines changed

5 files changed

+157
-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: 86 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,89 @@ 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+
}
386+
387+
public function testFullInitializationAfterPartialInitialization()
388+
{
389+
$counter = 0;
390+
$initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) {
391+
++$counter;
392+
393+
return 234;
394+
};
395+
$instance = ChildTestClass::createLazyGhost([
396+
'public' => $initializer,
397+
'publicReadonly' => $initializer,
398+
"\0*\0protected" => $initializer,
399+
"\0" => function ($obj, $defaults) use (&$instance, &$counter) {
400+
$counter += 1000;
401+
$this->assertSame($instance, $obj);
402+
95D9 403+
return [
404+
'public' => 345,
405+
'publicReadonly' => 456,
406+
"\0*\0protected" => 567,
407+
] + $defaults;
408+
},
409+
]);
410+
411+
$this->assertSame(234, $instance->public);
412+
$this->assertSame($instance, $instance->initializeLazyObject());
413+
$this->assertSame(234, $instance->public);
414+
$this->assertSame(456, $instance->publicReadonly);
415+
$this->assertSame(6, ((array) $instance)["\0".ChildTestClass::class."\0private"]);
416+
$this->assertSame(3, ((array) $instance)["\0".TestClass::class."\0private"]);
417+
$this->assertSame(1001, $counter);
418+
}
333419
}

0 commit comments

Comments
 (0)
0