Skip to content

Commit 2a00e60

Browse files
bug symfony#52762 [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior (nicolas-grekas)
This PR was merged into the 6.3 branch. Discussion ---------- [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | symfony#52753 | License | MIT This PR fixes another issue discovered with `@arnaud`-lb while working on lazy-objects. The root of the issue is described in php/php-src#12695 and this PR implements a fix based on a suggestion by `@iluuu1994` Only ghost objects are affected; proxy objects are OK. Commits ------- b051700 [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior
2 parents 706b67e + b051700 commit 2a00e60

File tree

5 files changed

+40
-19
lines changed

5 files changed

+40
-19
lines changed

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,10 @@ public static function getPropertyScopes($class)
271271
$name = $property->name;
272272

273273
if (\ReflectionProperty::IS_PRIVATE & $flags) {
274-
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null];
274+
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null, $property];
275275
continue;
276276
}
277-
$propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $property->class : null];
277+
$propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $property->class : null, $property];
278278

279279
if (\ReflectionProperty::IS_PROTECTED & $flags) {
280280
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
@@ -288,8 +288,8 @@ public static function getPropertyScopes($class)
288288
if (!$property->isStatic()) {
289289
$name = $property->name;
290290
$readonlyScope = $property->isReadOnly() ? $class : null;
291-
$propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope];
292-
$propertyScopes[$name] ??= [$class, $name, $readonlyScope];
291+
$propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope, $property];
292+
$propertyScopes[$name] ??= [$class, $name, $readonlyScope, $property];
293293
}
294294
}
295295
}

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -65,29 +65,32 @@ public function initialize($instance, $propertyName, $propertyScope)
6565
return $this->status = self::STATUS_INITIALIZED_PARTIAL;
6666
}
6767

68-
$status = self::STATUS_UNINITIALIZED_PARTIAL;
69-
7068
if ($initializer = $this->initializer["\0"] ?? null) {
7169
if (!\is_array($values = $initializer($instance, LazyObjectRegistry::$defaultProperties[$class]))) {
7270
throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values)));
7371
}
7472
$properties = (array) $instance;
7573
foreach ($values as $key => $value) {
76-
if ($k === $key) {
77-
$status = self::STATUS_INITIALIZED_PARTIAL;
78-
}
7974
if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) {
8075
$scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class);
8176
$accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope);
8277
$accessor['set']($instance, $name, $value);
78+
79+
if ($k === $key) {
80+
$this->status = self::STATUS_INITIALIZED_PARTIAL;
81+
}
8382
}
8483
}
8584
}
8685

87-
return $status;
86+
return $this->status;
87+
}
88+
89+
if (self::STATUS_INITIALIZED_PARTIAL === $this->status) {
90+
return self::STATUS_INITIALIZED_PARTIAL;
8891
}
8992

90-
$this->status = self::STATUS_INITIALIZED_FULL;
93+
$this->status = self::STATUS_INITIALIZED_PARTIAL;
9194

9295
try {
9396
if ($defaultProperties = array_diff_key(LazyObjectRegistry::$defaultProperties[$instance::class], $this->skippedProperties)) {
@@ -102,7 +105,7 @@ public function initialize($instance, $propertyName, $propertyScope)
102105
throw $e;
103106
}
104107

105-
return self::STATUS_INITIALIZED_FULL;
108+
return $this->status = self::STATUS_INITIALIZED_FULL;
106109
}
107110

108111
public function reset($instance): void

src/Symfony/Component/VarExporter/LazyGhostTrait.php

+19-6
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,18 @@ public function &__get($name): mixed
172172
$scope = Registry::getScope($propertyScopes, $class, $name);
173173
$state = $this->lazyObjectState ?? null;
174174

175-
if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))
176-
&& LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)
177-
) {
178-
goto get_in_scope;
175+
if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) {
176+
if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) {
177+
// Work around php/php-src#12695
178+
$property = $propertyScopes[null === $scope ? $name : "\0$scope\0$name"][3]
179+
?? (Hydrator::$propertyScopes[$this::class] = Hydrator::getPropertyScopes($this::class))[3];
180+
} else {
181+
$property = null;
182+
}
183+
184+
if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)) {
185+
goto get_in_scope;
186+
}
179187
}
180188
}
181189

@@ -237,7 +245,9 @@ public function __set($name, $value): void
237245
$scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope);
238246
$state = $this->lazyObjectState ?? null;
239247

240-
if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) {
248+
if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
249+
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
250+
) {
241251
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
242252
$state->initialize($this, $name, $readonlyScope ?? $scope);
243253
}
@@ -271,6 +281,7 @@ public function __isset($name): bool
271281
$state = $this->lazyObjectState ?? null;
272282

273283
if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))
284+
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
274285
&& LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)
275286
) {
276287
goto isset_in_scope;
@@ -300,7 +311,9 @@ public function __unset($name): void
300311
$scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope);
301312
$state = $this->lazyObjectState ?? null;
302313

303-
if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) {
314+
if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
315+
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
316+
) {
304317
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
305318
$state->initialize($this, $name, $readonlyScope ?? $scope);
306319
}

src/Symfony/Component/VarExporter/ProxyHelper.php

+3
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ private static function exportPropertyScopes(string $parent): string
322322
{
323323
$propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent);
324324
uksort($propertyScopes, 'strnatcmp');
325+
foreach ($propertyScopes as $k => $v) {
326+
unset($propertyScopes[$k][3]);
327+
}
325328
$propertyScopes = VarExporter::export($propertyScopes);
326329
$propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes);
327330
$propertyScopes = preg_replace("/(?|(,)\n( ) |\n |,\n (\]))/", '$1$2', $propertyScopes);

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ public function testUnsetPublic()
6565

6666
$this->assertSame(["\0".TestClass::class."\0lazyObjectState"], array_keys((array) $instance));
6767
unset($instance->public);
68-
$this->assertFalse(isset($instance->public));
6968
$this->assertSame(4, $instance->publicReadonly);
69+
$this->expectException(\BadMethodCallException::class);
70+
$this->expectExceptionMessage('__isset(public)');
71+
isset($instance->public);
7072
}
7173

7274
public function testSetPublic()

0 commit comments

Comments
 (0)