diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php index d5c3738a62a0b..81dd1a0b9c9cb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy.php @@ -76,7 +76,7 @@ class WitherProxy580fe0f extends \Symfony\Component\DependencyInjection\Tests\Co use \Symfony\Component\VarExporter\LazyProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = [ - 'foo' => [parent::class, 'foo', null], + 'foo' => [parent::class, 'foo', null, 4], ]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php index 0867347a6f845..8952ebd6d8ac9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_wither_lazy_non_shared.php @@ -78,7 +78,7 @@ class WitherProxyDd381be extends \Symfony\Component\DependencyInjection\Tests\Co use \Symfony\Component\VarExporter\LazyProxyTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = [ - 'foo' => [parent::class, 'foo', null], + 'foo' => [parent::class, 'foo', null, 4], ]; } diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index dc4a9feaf8556..86b05b91727d2 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -20,7 +20,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "require-dev": { "symfony/yaml": "^5.4|^6.0|^7.0", diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index 38cf3c5d866f0..21e3f5816e9de 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -145,7 +145,8 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $i = 0; $n = (string) $name; if ('' === $n || "\0" !== $n[0]) { - $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'; } elseif ('*' === $n[1]) { $n = substr($n, 3); $c = $reflector->getProperty($n)->class; diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 5b1d43924fc94..d8250d44b4238 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -20,6 +20,9 @@ */ class Hydrator { + public const PROPERTY_HAS_HOOKS = 1; + public const PROPERTY_NOT_BY_REF = 2; + public static array $hydrators = []; public static array $simpleHydrators = []; public static array $propertyScopes = []; @@ -156,13 +159,16 @@ public static function getHydrator($class) public static function getSimpleHydrator($class) { $baseHydrator = self::$simpleHydrators['stdClass'] ??= (function ($properties, $object) { - $readonly = (array) $this; + $notByRef = (array) $this; foreach ($properties as $name => &$value) { - $object->$name = $value; - - if (!($readonly[$name] ?? false)) { + if (!$noRef = $notByRef[$name] ?? false) { + $object->$name = $value; $object->$name = &$value; + } elseif (true !== $noRef) { + $notByRef($object, $value); + } else { + $object->$name = $value; } } })->bindTo(new \stdClass()); @@ -217,14 +223,19 @@ public static function getSimpleHydrator($class) } if (!$classReflector->isInternal()) { - $readonly = new \stdClass(); - foreach ($classReflector->getProperties(\ReflectionProperty::IS_READONLY) as $propertyReflector) { - if ($class === $propertyReflector->class) { - $readonly->{$propertyReflector->name} = true; + $notByRef = new \stdClass(); + foreach ($classReflector->getProperties() as $propertyReflector) { + if ($propertyReflector->isStatic()) { + continue; + } + if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) { + $notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...); + } elseif ($propertyReflector->isReadOnly()) { + $notByRef->{$propertyReflector->name} = true; } } - return $baseHydrator->bindTo($readonly, $class); + return $baseHydrator->bindTo($notByRef, $class); } if ($classReflector->name !== $class) { @@ -269,26 +280,26 @@ public static function getPropertyScopes($class) continue; } $name = $property->name; + $access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0); + + if (\PHP_VERSION_ID >= 80400 && !$property->isAbstract() && $h = $property->getHooks()) { + $access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0); + } if (\ReflectionProperty::IS_PRIVATE & $flags) { - $writeScope = null; - if (\PHP_VERSION_ID >= 80400 ? $property->isPrivateSet() : ($flags & \ReflectionProperty::IS_READONLY)) { - $writeScope = $class; - } - $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $writeScope, $property]; + $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, null, $access, $property]; continue; } - $writeScope = null; - if (\PHP_VERSION_ID >= 80400 ? $property->isProtectedSet() || $property->isPrivateSet() : ($flags & \ReflectionProperty::IS_READONLY)) { - $writeScope = $property->class; + + $propertyScopes[$name] = [$class, $name, null, $access, $property]; + + if ($flags & (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY)) { + $propertyScopes[$name][2] = $property->class; } - $propertyScopes[$name] = [$class, $name, $writeScope, $property]; if (\ReflectionProperty::IS_PROTECTED & $flags) { $propertyScopes["\0*\0$name"] = $propertyScopes[$name]; - } elseif (\PHP_VERSION_ID >= 80400 && $property->getHooks()) { - $propertyScopes[$name][4] = true; } } @@ -296,16 +307,20 @@ public static function getPropertyScopes($class) $class = $r->name; foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) { - if (!$property->isStatic()) { - $name = $property->name; - 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]; + $flags = $property->getModifiers(); + + if (\ReflectionProperty::IS_STATIC & $flags) { + continue; + } + $name = $property->name; + $access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0); + + if (\PHP_VERSION_ID >= 80400 && $h = $property->getHooks()) { + $access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0); } + + $propertyScopes["\0$class\0$name"] = [$class, $name, null, $access, $property]; + $propertyScopes[$name] ??= $propertyScopes["\0$class\0$name"]; } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index b9a8f82c4a4d0..d096be886ad81 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -58,14 +58,14 @@ public static function getClassResetters($class) $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); } - foreach ($propertyScopes as $key => [$scope, $name, $writeScope]) { + foreach ($propertyScopes as $key => [$scope, $name, $writeScope, $access]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; if ($k !== $key || "\0$class\0lazyObjectState" === $k) { continue; } - if ($k === $name && ($propertyScopes[$k][4] ?? false)) { + if ($access & Hydrator::PROPERTY_HAS_HOOKS) { $hookedProperties[$k] = true; } else { $classProperties[$writeScope ?? $scope][$name] = $key; @@ -101,8 +101,8 @@ public static function getClassResetters($class) public static function getClassAccessors($class) { return \Closure::bind(static fn () => [ - 'get' => static function &($instance, $name, $readonly) { - if (!$readonly) { + 'get' => static function &($instance, $name, $notByRef) { + if (!$notByRef) { return $instance->$name; } $value = $instance->$name; @@ -138,9 +138,9 @@ public static function getParentMethods($class) return $methods; } - public static function getScope($propertyScopes, $class, $property, $writeScope = null) + public static function getScopeForRead($propertyScopes, $class, $property) { - if (null === $writeScope && !isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) { + if (!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,27 @@ public static function getScope($propertyScopes, $class, $property, $writeScope if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) { $scope = $frame['object']->class; } - if (null === $writeScope && '*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { + if ('*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) { + return null; + } + + return $scope; + } + + public static function getScopeForWrite($propertyScopes, $class, $property, $flags) + { + if (!($flags & (\ReflectionProperty::IS_PRIVATE | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_READONLY | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET | \ReflectionProperty::IS_PROTECTED_SET : 0)))) { + return null; + } + $frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]; + + if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) { + $scope = $frame['object']->class; + } + if ($flags & (\ReflectionProperty::IS_PRIVATE | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY))) { + return $scope; + } + if ($flags & (\ReflectionProperty::IS_PROTECTED | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PROTECTED_SET : 0)) && ($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 9cb9b3d3cf64e..6ec8478a4ce13 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -45,7 +45,7 @@ public function __construct(public readonly \Closure|array $initializer, $skippe $this->status = \is_array($initializer) ? self::STATUS_UNINITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL; } - public function initialize($instance, $propertyName, $propertyScope) + public function initialize($instance, $propertyName, $writeScope) { if (self::STATUS_INITIALIZED_FULL === $this->status) { return self::STATUS_INITIALIZED_FULL; @@ -53,13 +53,13 @@ public function initialize($instance, $propertyName, $propertyScope) if (\is_array($this->initializer)) { $class = $instance::class; - $propertyScope ??= $class; + $writeScope ??= $class; $propertyScopes = Hydrator::$propertyScopes[$class]; - $propertyScopes[$k = "\0$propertyScope\0$propertyName"] ?? $propertyScopes[$k = "\0*\0$propertyName"] ?? $k = $propertyName; + $propertyScopes[$k = "\0$writeScope\0$propertyName"] ?? $propertyScopes[$k = "\0*\0$propertyName"] ?? $k = $propertyName; if ($initializer = $this->initializer[$k] ?? null) { - $value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); - $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); + $value = $initializer(...[$instance, $propertyName, $writeScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); + $accessor = LazyObjectRegistry::$classAccessors[$writeScope] ??= LazyObjectRegistry::getClassAccessors($writeScope); $accessor['set']($instance, $propertyName, $value); return $this->status = self::STATUS_INITIALIZED_PARTIAL; @@ -72,7 +72,7 @@ public function initialize($instance, $propertyName, $propertyScope) $properties = (array) $instance; foreach ($values as $key => $value) { if (!\array_key_exists($key, $properties) && [$scope, $name, $writeScope] = $propertyScopes[$key] ?? null) { - $scope = $writeScope ?? ('*' !== $scope ? $scope : $class); + $scope = $writeScope ?? $scope; $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, $writeScope]) { + foreach ($propertyScopes as $key => [$scope, $name, , $access]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - if ($k === $key && (null !== $writeScope || !\array_key_exists($k, $properties))) { + if ($k === $key && ($access & Hydrator::PROPERTY_HAS_HOOKS || ($access >> 2) & \ReflectionProperty::IS_READONLY || !\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 d97d2320ebc58..c2dbf99ce590c 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -116,7 +116,7 @@ public function initializeLazyObject(): static if (\array_key_exists($key, $properties) || ![$scope, $name, $writeScope] = $propertyScopes[$key] ?? null) { continue; } - $scope = $writeScope ?? ('*' !== $scope ? $scope : $class); + $scope = $writeScope ?? $scope; if (null === $values) { if (!\is_array($values = ($state->initializer["\0"])($this, Registry::$defaultProperties[$class]))) { @@ -160,20 +160,26 @@ public function &__get($name): mixed { $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; + $notByRef = 0; - if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) { + $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF; + if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) { // Work around php/php-src#12695 $property = null === $scope ? $name : "\0$scope\0$name"; - $property = $propertyScopes[$property][3] - ?? Hydrator::$propertyScopes[$this::class][$property][3] = new \ReflectionProperty($scope ?? $class, $name); + $property = $propertyScopes[$property][4] + ?? Hydrator::$propertyScopes[$this::class][$property][4] = new \ReflectionProperty($scope ?? $class, $name); } else { $property = null; } + if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) { + $scope ??= $writeScope; + } if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) { goto get_in_scope; @@ -199,7 +205,7 @@ public function &__get($name): mixed try { if (null === $scope) { - if (null === $writeScope) { + if (!$notByRef) { return $this->$name; } $value = $this->$name; @@ -208,7 +214,7 @@ public function &__get($name): mixed } $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); - return $accessor['get']($this, $name, null !== $writeScope); + return $accessor['get']($this, $name, $notByRef); } catch (\Error $e) { if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) { throw $e; @@ -223,7 +229,7 @@ public function &__get($name): mixed $accessor['set']($this, $name, []); - return $accessor['get']($this, $name, null !== $writeScope); + return $accessor['get']($this, $name, $notByRef); } 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,8 +245,8 @@ public function __set($name, $value): void $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); $state = $this->lazyObjectState ?? null; if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) @@ -275,7 +281,7 @@ public function __isset($name): bool $scope = null; if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); $state = $this->lazyObjectState ?? null; if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"])) @@ -305,8 +311,8 @@ public function __unset($name): void $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; - if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); $state = $this->lazyObjectState ?? null; if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index 8fccde2127085..1074c0cba0719 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -88,14 +88,19 @@ public function &__get($name): mixed $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class); $scope = null; $instance = $this; + $notByRef = 0; - if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF; + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { $instance = $state->realInstance ??= ($state->initializer)(); } + if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) { + $scope ??= $writeScope; + } $parent = 2; goto get_in_scope; } @@ -119,10 +124,11 @@ public function &__get($name): mixed } get_in_scope: + $notByRef = $notByRef || 1 === $parent; try { if (null === $scope) { - if (null === $writeScope && 1 !== $parent) { + if (!$notByRef) { return $instance->$name; } $value = $instance->$name; @@ -131,7 +137,7 @@ public function &__get($name): mixed } $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); - return $accessor['get']($instance, $name, null !== $writeScope || 1 === $parent); + return $accessor['get']($instance, $name, $notByRef); } catch (\Error $e) { if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) { throw $e; @@ -146,7 +152,7 @@ public function &__get($name): mixed $accessor['set']($instance, $name, []); - return $accessor['get']($instance, $name, null !== $writeScope || 1 === $parent); + return $accessor['get']($instance, $name, $notByRef); } catch (\Error) { throw $e; } @@ -159,8 +165,8 @@ public function __set($name, $value): void $scope = null; $instance = $this; - if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { @@ -195,7 +201,7 @@ public function __isset($name): bool $instance = $this; if ([$class] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name); + $scope = Registry::getScopeForRead($propertyScopes, $class, $name); if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { @@ -227,8 +233,8 @@ public function __unset($name): void $scope = null; $instance = $this; - if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) { - $scope = Registry::getScope($propertyScopes, $class, $name, $writeScope); + if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) { + $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2); if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) { if ($state = $this->lazyObjectState ?? null) { diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index 264c1af29e6ca..538d23f7c5087 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -61,30 +61,34 @@ public static function generateLazyGhost(\ReflectionClass $class): string $hooks = ''; $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); - foreach ($propertyScopes as $name => $scope) { - if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) { + foreach ($propertyScopes as $key => [$scope, $name, , $access]) { + $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; + $flags = $access >> 2; + + if ($k !== $key || !($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) { continue; } - if ($p->isFinal()) { - throw new LogicException(sprintf('Cannot generate lazy ghost: property "%s::$%s" is final.', $class->name, $p->name)); + if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) { + throw new LogicException(sprintf('Cannot generate lazy ghost: property "%s::$%s" is final or private(set).', $class->name, $name)); } + $p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name); + $type = self::exportType($p); - $hooks .= "\n public {$type} \${$name} {\n"; + $hooks .= "\n " + .($p->isProtected() ? 'protected' : 'public') + .($p->isProtectedSet() ? ' protected(set)' : '') + ." {$type} \${$name} {\n"; foreach ($p->getHooks() as $hook => $method) { - if ($method->isFinal()) { - throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is final.', $class->name, $method->name)); - } - if ('get' === $hook) { $ref = ($method->returnsReference() ? '&' : ''); - $hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n"; + $hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n"; } elseif ('set' === $hook) { $parameters = self::exportParameters($method, true); $arg = '$'.$method->getParameters()[0]->name; - $hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n"; + $hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n"; } else { throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name)); } @@ -134,17 +138,29 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf $abstractProperties = []; $hookedProperties = []; if (\PHP_VERSION_ID >= 80400 && $class) { - foreach ($propertyScopes as $name => $scope) { - if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) { - $abstractProperties[$name] = isset($scope[4]) && $p->isAbstract() ? $p : false; + foreach ($propertyScopes as $key => [$scope, $name, , $access]) { + $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; + $flags = $access >> 2; + + if ($k !== $key) { continue; } - if ($p->isFinal()) { - throw new LogicException(\sprintf('Cannot generate lazy proxy: property "%s::$%s" is final.', $class->name, $p->name)); + if ($flags & \ReflectionProperty::IS_ABSTRACT) { + $abstractProperties[$name] = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name); + continue; } - $abstractProperties[$name] = false; + + if (!($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) { + continue; + } + + if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) { + throw new LogicException(sprintf('Cannot generate lazy proxy: property "%s::$%s" is final or private(set).', $class->name, $name)); + } + + $p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name); $hookedProperties[$name] = [$p, $p->getHooks()]; } } @@ -169,51 +185,52 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf foreach (array_filter($abstractProperties) as $name => $p) { $type = self::exportType($p); - $hooks .= "\n public {$type} \${$name};\n"; - unset($propertyScopes[$name][4]); + $hooks .= "\n " + .($p->isProtected() ? 'protected' : 'public') + .($p->isProtectedSet() ? ' protected(set)' : '') + ." {$type} \${$name};\n"; } foreach ($hookedProperties as $name => [$p, $methods]) { $type = self::exportType($p); - $hooks .= "\n public {$type} \${$p->name} {\n"; + $hooks .= "\n " + .($p->isProtected() ? 'protected' : 'public') + .($p->isProtectedSet() ? ' protected(set)' : '') + ." {$type} \${$name} {\n"; foreach ($methods as $hook => $method) { - if ($method->isFinal()) { - throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is final.', $class->name, $method->name)); - } - if ('get' === $hook) { $ref = ($method->returnsReference() ? '&' : ''); $hooks .= <<lazyObjectState)) { - return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name}; - } - - return parent::\${$p->name}::get(); + {$ref}get { + if (isset(\$this->lazyObjectState)) { + return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name}; } + return parent::\${$p->name}::get(); + } + EOPHP; } elseif ('set' === $hook) { $parameters = self::exportParameters($method, true); $arg = '$'.$method->getParameters()[0]->name; $hooks .= <<lazyObjectState)) { - \$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)(); - \$this->lazyObjectState->realInstance->{$p->name} = {$arg}; - } - - parent::\${$p->name}::set({$arg}); + set({$parameters}) { + if (isset(\$this->lazyObjectState)) { + \$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)(); + \$this->lazyObjectState->realInstance->{$p->name} = {$arg}; } + parent::\${$p->name}::set({$arg}); + } + EOPHP; } else { throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name)); } } - $hooks .= " }\n"; + $hooks .= " }\n"; } $extendsInternalClass = false; @@ -469,7 +486,7 @@ private static function exportPropertyScopes(string $parent, array $propertyScop { uksort($propertyScopes, 'strnatcmp'); foreach ($propertyScopes as $k => $v) { - unset($propertyScopes[$k][3]); + unset($propertyScopes[$k][4]); } $propertyScopes = VarExporter::export($propertyScopes); $propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes); diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/BackedProperty.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/BackedProperty.php new file mode 100644 index 0000000000000..5c5d7688f97ca --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/BackedProperty.php @@ -0,0 +1,24 @@ + + * + * 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; + +class BackedProperty +{ + public private(set) string $name { + get => $this->name; + set => $value; + } + public function __construct(string $name) + { + $this->name = $name; + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php index ca6b235eba66d..6cac9ffc03d01 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyGhost/ChildMagicClass.php @@ -18,14 +18,5 @@ class ChildMagicClass extends MagicClass implements LazyObjectInterface { use LazyGhostTrait; - private const LAZY_OBJECT_PROPERTY_SCOPES = [ - "\0".self::class."\0".'data' => [self::class, 'data', null], - "\0".self::class."\0".'lazyObjectState' => [self::class, 'lazyObjectState', null], - "\0".parent::class."\0".'data' => [parent::class, 'data', null], - 'cloneCounter' => [self::class, 'cloneCounter', null], - 'data' => [self::class, 'data', null], - 'lazyObjectState' => [self::class, 'lazyObjectState', null], - ]; - private int $data = 123; } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php index d6029113c647b..a912ca403ca26 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AsymmetricVisibility.php @@ -13,10 +13,14 @@ class AsymmetricVisibility { - public private(set) int $foo; + public function __construct( + public private(set) int $foo, + private readonly int $bar, + ) { + } - public function __construct(int $foo) + public function getBar(): int { - $this->foo = $foo; + return $this->bar; } } diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/backed-property.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/backed-property.php new file mode 100644 index 0000000000000..bcbc5729e9e5b --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/backed-property.php @@ -0,0 +1,17 @@ + [ + 'name' => [ + 'name', + ], + ], + ], + $o[0], + [] +); diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 12a7d19a381be..5b80f6b00339b 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -510,13 +510,18 @@ public function testPropertyHooks() */ public function testAsymmetricVisibility() { - $initialized = false; - $object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) use (&$initialized) { - $initialized = true; + $object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) { + $instance->__construct(123, 234); + }); + + $this->assertSame(123, $object->foo); + $this->assertSame(234, $object->getBar()); - $instance->__construct(123); + $object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) { + $instance->__construct(123, 234); }); + $this->assertSame(234, $object->getBar()); $this->assertSame(123, $object->foo); } diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index cf1f625b8f4ff..61be7429fb0cd 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -369,13 +369,18 @@ public function testAbstractPropertyHooks() */ public function testAsymmetricVisibility() { - $initialized = false; - $object = $this->createLazyProxy(AsymmetricVisibility::class, function () use (&$initialized) { - $initialized = true; + $object = $this->createLazyProxy(AsymmetricVisibility::class, function () { + return new AsymmetricVisibility(123, 234); + }); + + $this->assertSame(123, $object->foo); + $this->assertSame(234, $object->getBar()); - return new AsymmetricVisibility(123); + $object = $this->createLazyProxy(AsymmetricVisibility::class, function () { + return new AsymmetricVisibility(123, 234); }); + $this->assertSame(234, $object->getBar()); $this->assertSame(123, $object->foo); } diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index a8dcc21084c66..874dd593b8460 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -253,10 +253,9 @@ public function testNullStandaloneReturnType() */ public function testPropertyHooks() { - self::assertStringContainsString( - "[parent::class, 'backed', null, 4 => true]", - ProxyHelper::generateLazyProxy(new \ReflectionClass(Hooked::class)) - ); + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(Hooked::class)); + self::assertStringContainsString("'backed' => [parent::class, 'backed', null, 7],", $proxyCode); + self::assertStringContainsString("'notBacked' => [parent::class, 'notBacked', null, 2055],", $proxyCode); } } diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index 16c87b040d6b6..29fcf7598553b 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -16,6 +16,7 @@ use Symfony\Component\VarExporter\Exception\ClassNotFoundException; use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; use Symfony\Component\VarExporter\Internal\Registry; +use Symfony\Component\VarExporter\Tests\Fixtures\BackedProperty; use Symfony\Component\VarExporter\Tests\Fixtures\FooReadonly; use Symfony\Component\VarExporter\Tests\Fixtures\FooSerializable; use Symfony\Component\VarExporter\Tests\Fixtures\FooUnitEnum; @@ -239,6 +240,12 @@ public static function provideExport() yield ['unit-enum', [FooUnitEnum::Bar], true]; yield ['readonly', new FooReadonly('k', 'v')]; + + if (\PHP_VERSION_ID < 80400) { + return; + } + + yield ['backed-property', new BackedProperty('name')]; } public function testUnicodeDirectionality()