From efd69663c71a4cc7c2c038df1b977ace1e86fd8c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 13 Nov 2022 15:33:19 +0100 Subject: [PATCH] [VarExporter] Use array for partial initialization of lazy ghost objects --- .../Internal/LazyObjectRegistry.php | 8 +- .../VarExporter/Internal/LazyObjectState.php | 31 ++++---- .../Component/VarExporter/LazyGhostTrait.php | 75 +++++++++---------- .../Component/VarExporter/LazyProxyTrait.php | 4 +- .../VarExporter/Tests/LazyGhostTraitTest.php | 64 ++++++++++------ 5 files changed, 99 insertions(+), 83 deletions(-) diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index 8f67db33ce1ba..3951866b57219 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -70,18 +70,18 @@ public static function getClassResetters($class) $resetters = []; foreach ($classProperties as $scope => $properties) { - $resetters[] = \Closure::bind(static function ($instance, $skippedProperties = []) use ($properties) { + $resetters[] = \Closure::bind(static function ($instance, $skippedProperties, $onlyProperties = null) use ($properties) { foreach ($properties as $name => $key) { - if (!\array_key_exists($key, $skippedProperties)) { + if (!\array_key_exists($key, $skippedProperties) && (null === $onlyProperties || \array_key_exists($key, $onlyProperties))) { unset($instance->$name); } } }, null, $scope); } - $resetters[] = static function ($instance, $skippedProperties = []) { + $resetters[] = static function ($instance, $skippedProperties, $onlyProperties = null) { foreach ((array) $instance as $name => $value) { - if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties)) { + if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties) && (null === $onlyProperties || \array_key_exists($name, $onlyProperties))) { unset($instance->$name); } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index 1e11f15fae92a..605f1fdd52831 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -22,9 +22,10 @@ */ class LazyObjectState { - public const STATUS_INITIALIZED_PARTIAL = 1; - public const STATUS_UNINITIALIZED_FULL = 2; + public const STATUS_UNINITIALIZED_FULL = 1; + public const STATUS_UNINITIALIZED_PARTIAL = 2; public const STATUS_INITIALIZED_FULL = 3; + public const STATUS_INITIALIZED_PARTIAL = 4; /** * @var array @@ -36,37 +37,34 @@ class LazyObjectState */ public int $status = 0; - public function __construct(public \Closure $initializer, $skippedProperties = []) + public function __construct(public readonly \Closure|array $initializer, $skippedProperties = []) { $this->skippedProperties = $skippedProperties; + $this->status = \is_array($initializer) ? self::STATUS_UNINITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL; } public function initialize($instance, $propertyName, $propertyScope) { - if (!$this->status) { - $this->status = 4 <= (new \ReflectionFunction($this->initializer))->getNumberOfParameters() ? self::STATUS_INITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL; - - if (null === $propertyName) { - return $this->status; - } - } - if (self::STATUS_INITIALIZED_FULL === $this->status) { return self::STATUS_INITIALIZED_FULL; } - if (self::STATUS_INITIALIZED_PARTIAL === $this->status) { + if (\is_array($this->initializer)) { $class = $instance::class; $propertyScope ??= $class; $propertyScopes = Hydrator::$propertyScopes[$class]; $propertyScopes[$k = "\0$propertyScope\0$propertyName"] ?? $propertyScopes[$k = "\0*\0$propertyName"] ?? $k = $propertyName; - $value = ($this->initializer)(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); + if (!$initializer = $this->initializer[$k] ?? null) { + return self::STATUS_UNINITIALIZED_PARTIAL; + } + + $value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); $accessor['set']($instance, $propertyName, $value); - return self::STATUS_INITIALIZED_PARTIAL; + return $this->status = self::STATUS_INITIALIZED_PARTIAL; } $this->status = self::STATUS_INITIALIZED_FULL; @@ -93,6 +91,7 @@ public function reset($instance): void $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); $skippedProperties = $this->skippedProperties; $properties = (array) $instance; + $onlyProperties = \is_array($this->initializer) ? $this->initializer : null; foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; @@ -103,7 +102,9 @@ public function reset($instance): void } foreach (LazyObjectRegistry::$classResetters[$class] as $reset) { - $reset($instance, $skippedProperties); + $reset($instance, $skippedProperties, $onlyProperties); } + + $this->status = self::STATUS_INITIALIZED_FULL === $this->status ? self::STATUS_UNINITIALIZED_FULL : self::STATUS_UNINITIALIZED_PARTIAL; } } diff --git a/src/Symfony/Component/VarExporter/LazyGhostTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php index 121a495c24f5c..16e40f7234567 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -16,28 +16,26 @@ use Symfony\Component\VarExporter\Internal\LazyObjectState; /** - * @property int $lazyObjectId This property must be declared in classes using this trait + * @property int $lazyObjectId This property must be declared as private in classes using this trait */ trait LazyGhostTrait { /** * Creates a lazy-loading ghost instance. * - * The initializer can take two forms. In both forms, - * the instance to initialize is passed as first argument. + * When the initializer is a closure, it should initialize all properties at + * once and is given the instance to initialize as argument. * - * When the initializer takes only one argument, it is expected to initialize all - * properties at once. + * When the initializer is an array of closures, it should be indexed by + * properties and closures should accept 4 arguments: the instance to + * initialize, the property to initialize, its write-scope, and its default + * value. Each closure should return the value of the corresponding property. * - * When 4 arguments are required, the initializer is expected to return the value - * of each property one by one. The extra arguments are the name of the property - * to initialize, the write-scope of that property, and its default value. - * - * @param \Closure(static):void|\Closure(static, string, ?string, mixed):mixed $initializer - * @param array $skippedProperties An array indexed by the properties to skip, - * aka the ones that the initializer doesn't set + * @param \Closure(static):void|array $initializer + * @param array $skippedProperties An array indexed by the properties to skip, aka the ones + * that the initializer doesn't set when its a closure */ - public static function createLazyGhost(\Closure $initializer, array $skippedProperties = [], self $instance = null): static + public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = [], self $instance = null): static { if (self::class !== $class = $instance ? $instance::class : static::class) { $skippedProperties["\0".self::class."\0lazyObjectId"] = true; @@ -49,9 +47,10 @@ public static function createLazyGhost(\Closure $initializer, array $skippedProp Registry::$defaultProperties[$class] ??= (array) $instance; $instance->lazyObjectId = $id = spl_object_id($instance); Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties); + $onlyProperties = \is_array($initializer) ? $initializer : null; foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) { - $reset($instance, $skippedProperties); + $reset($instance, $skippedProperties, $onlyProperties); } return $instance; @@ -66,17 +65,15 @@ public function isLazyObjectInitialized(): bool return true; } - if (LazyObjectState::STATUS_INITIALIZED_PARTIAL !== $state->status) { + if (!\is_array($state->initializer)) { return LazyObjectState::STATUS_INITIALIZED_FULL === $state->status; } $class = $this::class; $properties = (array) $this; $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); - foreach ($propertyScopes as $key => [$scope, $name]) { - $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - - if ($k === $key && !\array_key_exists($k, $properties)) { + foreach ($state->initializer as $key => $initializer) { + if (!\array_key_exists($key, $properties) && isset($propertyScopes[$key])) { return false; } } @@ -93,7 +90,7 @@ public function initializeLazyObject(): static return $this; } - if (LazyObjectState::STATUS_INITIALIZED_PARTIAL !== ($state->status ?: $state->initialize($this, null, null))) { + if (!\is_array($state->initializer)) { if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { $state->initialize($this, '', null); } @@ -104,10 +101,8 @@ public function initializeLazyObject(): static $class = $this::class; $properties = (array) $this; $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); - foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { - $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0".($scope = '*')."\0$name"] ?? $k = $name; - - if ($k !== $key || \array_key_exists($k, $properties)) { + foreach ($state->initializer as $key => $initializer) { + if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { continue; } @@ -127,14 +122,8 @@ public function resetLazyObject(): bool return false; } - if (!$state->status) { - return $state->initialize($this, null, null) || true; - } - - $state->reset($this); - - if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) { - $state->status = LazyObjectState::STATUS_UNINITIALIZED_FULL; + if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $state->status) { + $state->reset($this); } return true; @@ -149,8 +138,9 @@ public function &__get($name): mixed $scope = Registry::getScope($propertyScopes, $class, $name); $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; - if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) { - $state->initialize($this, $name, $readonlyScope ?? $scope); + if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"])) + && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope) + ) { goto get_in_scope; } } @@ -192,10 +182,10 @@ public function __set($name, $value): void if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) { $scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope); - $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; + if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) { - if (LazyObjectState::STATUS_UNINITIALIZED_FULL === ($state->status ?: $state->initialize($this, null, null))) { + if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { $state->initialize($this, $name, $readonlyScope ?? $scope); } goto set_in_scope; @@ -227,8 +217,9 @@ public function __isset($name): bool $scope = Registry::getScope($propertyScopes, $class, $name); $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; - if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) { - $state->initialize($this, $name, $readonlyScope ?? $scope); + if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"])) + && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope) + ) { goto isset_in_scope; } } @@ -257,7 +248,7 @@ public function __unset($name): void $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))) { - if (LazyObjectState::STATUS_UNINITIALIZED_FULL === ($state->status ?: $state->initialize($this, null, null))) { + if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) { $state->initialize($this, $name, $readonlyScope ?? $scope); } goto unset_in_scope; @@ -328,7 +319,7 @@ public function __destruct() $state = Registry::$states[$this->lazyObjectId ?? ''] ?? null; try { - if ($state && !\in_array($state->status, [LazyObjectState::STATUS_INITIALIZED_FULL, LazyObjectState::STATUS_INITIALIZED_PARTIAL], true)) { + if ($state && \in_array($state->status, [LazyObjectState::STATUS_UNINITIALIZED_FULL, LazyObjectState::STATUS_UNINITIALIZED_PARTIAL], true)) { return; } @@ -344,7 +335,9 @@ public function __destruct() private function setLazyObjectAsInitialized(bool $initialized): void { - if ($state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { + $state = Registry::$states[$this->lazyObjectId ?? '']; + + if ($state && !\is_array($state->initializer)) { $state->status = $initialized ? LazyObjectState::STATUS_INITIALIZED_FULL : LazyObjectState::STATUS_UNINITIALIZED_FULL; } } diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index 638a6482a201c..4b58b7a388160 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -17,8 +17,8 @@ use Symfony\Component\VarExporter\Internal\LazyObjectState; /** - * @property int $lazyObjectId This property must be declared in classes using this trait - * @property parent $lazyObjectReal This property must be declared in classes using this trait; + * @property int $lazyObjectId This property must be declared as private in classes using this trait + * @property parent $lazyObjectReal This property must be declared as private in classes using this trait; * its type should match the type of the proxied object */ trait LazyProxyTrait diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 113c6f4f8c7fb..a64747490bd0c 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -210,20 +210,39 @@ public function testFullInitialization() public function testPartialInitialization() { $counter = 0; - $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { - ++$counter; - - return match ($property) { - 'public' => 4 === $default ? 123 : -1, - 'publicReadonly' => 234, - 'protected' => 5 === $default ? 345 : -1, - 'protectedReadonly' => 456, - 'private' => match ($scope) { - TestClass::class => 3 === $default ? 567 : -1, - ChildTestClass::class => 6 === $default ? 678 : -1, - }, - }; - }); + $instance = ChildTestClass::createLazyGhost([ + 'public' => static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 4 === $default ? 123 : -1; + }, + 'publicReadonly' => static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 234; + }, + "\0*\0protected" => static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 5 === $default ? 345 : -1; + }, + "\0*\0protectedReadonly" => static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 456; + }, + "\0".TestClass::class."\0private" => static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 3 === $default ? 567 : -1; + }, + "\0".ChildTestClass::class."\0private" => static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 6 === $default ? 678 : -1; + }, + 'dummyProperty' => fn () => 123, + ]); $this->assertSame(["\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $this->assertFalse($instance->isLazyObjectInitialized()); @@ -246,9 +265,14 @@ public function testPartialInitialization() public function testPartialInitializationWithReset() { - $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) { + $initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) { return 234; - }); + }; + $instance = ChildTestClass::createLazyGhost([ + 'public' => $initializer, + 'publicReadonly' => $initializer, + "\0*\0protected" => $initializer, + ]); $r = new \ReflectionProperty($instance, 'public'); $r->setValue($instance, 123); @@ -262,9 +286,7 @@ public function testPartialInitializationWithReset() $this->assertSame(234, $instance->publicReadonly); $this->assertSame(234, $instance->public); - $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) { - return 234; - }); + $instance = ChildTestClass::createLazyGhost(['public' => $initializer]); $instance->resetLazyObject(); @@ -277,9 +299,9 @@ public function testPartialInitializationWithReset() public function testPartialInitializationWithNastyPassByRef() { - $instance = ChildTestClass::createLazyGhost(function (ChildTestClass $instance, string &$property, ?string &$scope, mixed $default) { + $instance = ChildTestClass::createLazyGhost(['public' => function (ChildTestClass $instance, string &$property, ?string &$scope, mixed $default) { return $property = $scope = 123; - }); + }]); $this->assertSame(123, $instance->public); }