From 7321bbfeb2e6ec6d5247d53b1c8748a6b783b47e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 28 Feb 2025 17:05:25 +0100 Subject: [PATCH] [VarExporter] Fix support for asymmetric visibility --- .../Component/VarExporter/Hydrator.php | 4 +-- .../VarExporter/Internal/Exporter.php | 3 +- .../VarExporter/Internal/Hydrator.php | 26 ++++++++------ .../Internal/LazyObjectRegistry.php | 10 +++--- .../VarExporter/Internal/LazyObjectState.php | 8 ++--- .../Component/VarExporter/LazyGhostTrait.php | 34 +++++++++---------- .../Component/VarExporter/LazyProxyTrait.php | 20 +++++------ .../LazyProxy/AsymmetricVisibility.php | 22 ++++++++++++ .../VarExporter/Tests/LazyGhostTraitTest.php | 16 +++++++++ .../VarExporter/Tests/LazyProxyTraitTest.php | 17 +++++++++- 10 files changed, 109 insertions(+), 51 deletions(-) create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php diff --git a/src/Symfony/Component/VarExporter/Hydrator.php b/src/Symfony/Component/VarExporter/Hydrator.php index 5f456fb3cf7e7..b718921d9f892 100644 --- a/src/Symfony/Component/VarExporter/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Hydrator.php @@ -61,8 +61,8 @@ public static function hydrate(object $instance, array $properties = [], array $ $propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class); foreach ($properties as $name => &$value) { - [$scope, $name, $readonlyScope] = $propertyScopes[$name] ?? [$class, $name, $class]; - $scopedProperties[$readonlyScope ?? $scope][$name] = &$value; + [$scope, $name, $writeScope] = $propertyScopes[$name] ?? [$class, $name, $class]; + $scopedProperties[$writeScope ?? $scope][$name] = &$value; } unset($value); } diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index ec711e1ed096b..38cf3c5d866f0 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -90,7 +90,8 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $properties = $serializeProperties; } else { foreach ($serializeProperties as $n => $v) { - $c = $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; + $p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; + $c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; $properties[$c][$n] = $v; } } diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 30636ab94a7ab..5b1d43924fc94 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -271,19 +271,19 @@ public static function getPropertyScopes($class) $name = $property->name; if (\ReflectionProperty::IS_PRIVATE & $flags) { - $readonlyScope = null; - if ($flags & \ReflectionProperty::IS_READONLY) { - $readonlyScope = $class; + $writeScope = null; + if (\PHP_VERSION_ID >= 80400 ? $property->isPrivateSet() : ($flags & \ReflectionProperty::IS_READONLY)) { + $writeScope = $class; } - $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $readonlyScope, $property]; + $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $writeScope, $property]; continue; } - $readonlyScope = null; - if ($flags & \ReflectionProperty::IS_READONLY) { - $readonlyScope = $property->class; + $writeScope = null; + if (\PHP_VERSION_ID >= 80400 ? $property->isProtectedSet() || $property->isPrivateSet() : ($flags & \ReflectionProperty::IS_READONLY)) { + $writeScope = $property->class; } - $propertyScopes[$name] = [$class, $name, $readonlyScope, $property]; + $propertyScopes[$name] = [$class, $name, $writeScope, $property]; if (\ReflectionProperty::IS_PROTECTED & $flags) { $propertyScopes["\0*\0$name"] = $propertyScopes[$name]; @@ -298,9 +298,13 @@ public static function getPropertyScopes($class) foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) { if (!$property->isStatic()) { $name = $property->name; - $readonlyScope = $property->isReadOnly() ? $class : null; - $propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope, $property]; - $propertyScopes[$name] ??= [$class, $name, $readonlyScope, $property]; + if (\PHP_VERSION_ID < 80400) { + $writeScope = $property->isReadOnly() ? $class : null; + } else { + $writeScope = $property->isPrivateSet() ? $class : null; + } + $propertyScopes["\0$class\0$name"] = [$class, $name, $writeScope, $property]; + $propertyScopes[$name] ??= [$class, $name, $writeScope, $property]; } } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index a7b4987e3b0db..b9a8f82c4a4d0 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -58,7 +58,7 @@ public static function getClassResetters($class) $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); } - foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { + foreach ($propertyScopes as $key => [$scope, $name, $writeScope]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; if ($k !== $key || "\0$class\0lazyObjectState" === $k) { @@ -68,7 +68,7 @@ public static function getClassResetters($class) if ($k === $name && ($propertyScopes[$k][4] ?? false)) { $hookedProperties[$k] = true; } else { - $classProperties[$readonlyScope ?? $scope][$name] = $key; + $classProperties[$writeScope ?? $scope][$name] = $key; } } @@ -138,9 +138,9 @@ public static function getParentMethods($class) return $methods; } - public static function getScope($propertyScopes, $class, $property, $readonlyScope = null) + public static function getScope($propertyScopes, $class, $property, $writeScope = null) { - if (null === $readonlyScope && !isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) { + if (null === $writeScope && !isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) { return null; } $frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]; @@ -148,7 +148,7 @@ public static function getScope($propertyScopes, $class, $property, $readonlySco if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) { $scope = $frame['object']->class; } - if (null === $readonlyScope && '*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { + if (null === $writeScope && '*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { return null; } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index f47dea4d8e6f5..9cb9b3d3cf64e 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -71,8 +71,8 @@ public function initialize($instance, $propertyName, $propertyScope) } $properties = (array) $instance; foreach ($values as $key => $value) { - if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { - $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); + if (!\array_key_exists($key, $properties) && [$scope, $name, $writeScope] = $propertyScopes[$key] ?? null) { + $scope = $writeScope ?? ('*' !== $scope ? $scope : $class); $accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope); $accessor['set']($instance, $name, $value); @@ -116,10 +116,10 @@ public function reset($instance): void $properties = (array) $instance; $onlyProperties = \is_array($this->initializer) ? $this->initializer : null; - foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { + foreach ($propertyScopes as $key => [$scope, $name, $writeScope]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - if ($k === $key && (null !== $readonlyScope || !\array_key_exists($k, $properties))) { + if ($k === $key && (null !== $writeScope || !\array_key_exists($k, $properties))) { $skippedProperties[$k] = true; } } diff --git a/src/Symfony/Component/VarExporter/LazyGhostTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php index 5191b59e705f1..d97d2320ebc58 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -113,10 +113,10 @@ public function initializeLazyObject(): static $properties = (array) $this; $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); foreach ($state->initializer as $key => $initializer) { - if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { + if (\array_key_exists($key, $properties) || ![$scope, $name, $writeScope] = $propertyScopes[$key] ?? null) { continue; } - $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); + $scope = $writeScope ?? ('*' !== $scope ? $scope : $class); if (null === $values) { if (!\is_array($values = ($state->initializer["\0"])($this, Registry::$defaultProperties[$class]))) { @@ -161,7 +161,7 @@ public function &__get($name): mixed $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { $scope = Registry::getScope($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; @@ -175,7 +175,7 @@ public function &__get($name): mixed $property = null; } - if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)) { + if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) { goto get_in_scope; } } @@ -199,7 +199,7 @@ public function &__get($name): mixed try { if (null === $scope) { - if (null === $readonlyScope) { + if (null === $writeScope) { return $this->$name; } $value = $this->$name; @@ -208,7 +208,7 @@ public function &__get($name): mixed } $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); - return $accessor['get']($this, $name, null !== $readonlyScope); + return $accessor['get']($this, $name, null !== $writeScope); } catch (\Error $e) { if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) { throw $e; @@ -223,7 +223,7 @@ public function &__get($name): mixed $accessor['set']($this, $name, []); - return $accessor['get']($this, $name, null !== $readonlyScope); + return $accessor['get']($this, $name, null !== $writeScope); } catch (\Error) { if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) { throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious()); @@ -239,15 +239,15 @@ public function __set($name, $value): void $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); $state = $this->lazyObjectState ?? null; - if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) + if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status ) { if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { - $state->initialize($this, $name, $readonlyScope ?? $scope); + $state->initialize($this, $name, $writeScope ?? $scope); } goto set_in_scope; } @@ -274,13 +274,13 @@ public function __isset($name): bool $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { $scope = Registry::getScope($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"])) && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status - && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope) + && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope) ) { goto isset_in_scope; } @@ -305,15 +305,15 @@ public function __unset($name): void $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); $state = $this->lazyObjectState ?? null; - if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) + if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status ) { if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { - $state->initialize($this, $name, $readonlyScope ?? $scope); + $state->initialize($this, $name, $writeScope ?? $scope); } goto unset_in_scope; } diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index 2033670522ab4..8fccde2127085 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -89,7 +89,7 @@ public function &__get($name): mixed $scope = null; $instance = $this; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { $scope = Registry::getScope($propertyScopes, $class, $name); if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { @@ -122,7 +122,7 @@ public function &__get($name): mixed try { if (null === $scope) { - if (null === $readonlyScope && 1 !== $parent) { + if (null === $writeScope && 1 !== $parent) { return $instance->$name; } $value = $instance->$name; @@ -131,7 +131,7 @@ public function &__get($name): mixed } $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); - return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent); + return $accessor['get']($instance, $name, null !== $writeScope || 1 === $parent); } catch (\Error $e) { if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) { throw $e; @@ -146,7 +146,7 @@ public function &__get($name): mixed $accessor['set']($instance, $name, []); - return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent); + return $accessor['get']($instance, $name, null !== $writeScope || 1 === $parent); } catch (\Error) { throw $e; } @@ -159,10 +159,10 @@ public function __set($name, $value): void $scope = null; $instance = $this; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); - if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { $instance = $state->realInstance ??= ($state->initializer)(); } @@ -227,10 +227,10 @@ public function __unset($name): void $scope = null; $instance = $this; - if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); + if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); - if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { + if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { $instance = $state->realInstance ??= ($state->initializer)(); } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php new file mode 100644 index 0000000000000..d6029113c647b --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; + +class AsymmetricVisibility +{ + public private(set) int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 8351ec0c79a93..12a7d19a381be 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -25,6 +25,7 @@ use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\MagicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ReadOnlyClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\TestClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AsymmetricVisibility; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\SimpleObject; @@ -504,6 +505,21 @@ public function testPropertyHooks() $this->assertSame(345, $object->backed); } + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibility() + { + $initialized = false; + $object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) use (&$initialized) { + $initialized = true; + + $instance->__construct(123); + }); + + $this->assertSame(123, $object->foo); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index 4f0702fd97452..cf1f625b8f4ff 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -18,8 +18,8 @@ use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; -use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\RegularClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AbstractHooked; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AsymmetricVisibility; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass; @@ -364,6 +364,21 @@ public function testAbstractPropertyHooks() $this->assertTrue($initialized); } + /** + * @requires PHP 8.4 + */ + public function testAsymmetricVisibility() + { + $initialized = false; + $object = $this->createLazyProxy(AsymmetricVisibility::class, function () use (&$initialized) { + $initialized = true; + + return new AsymmetricVisibility(123); + }); + + $this->assertSame(123, $object->foo); + } + /** * @template T *