From cd5bf0cedbcfa5c5a91b106346c8bf68d2916ddf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 24 Nov 2022 18:21:16 +0100 Subject: [PATCH] [VarExporter] Improve partial-initialization API for ghost objects --- .../VarExporter/Internal/LazyObjectState.php | 32 +++++-- .../Component/VarExporter/LazyGhostTrait.php | 49 +++++++++-- .../VarExporter/LazyObjectInterface.php | 4 +- .../Component/VarExporter/LazyProxyTrait.php | 4 +- .../VarExporter/Tests/LazyGhostTraitTest.php | 86 +++++++++++++++++++ 5 files changed, 157 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php index 605f1fdd52831..99f721df2605c 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectState.php @@ -55,16 +55,34 @@ public function initialize($instance, $propertyName, $propertyScope) $propertyScopes = Hydrator::$propertyScopes[$class]; $propertyScopes[$k = "\0$propertyScope\0$propertyName"] ?? $propertyScopes[$k = "\0*\0$propertyName"] ?? $k = $propertyName; - if (!$initializer = $this->initializer[$k] ?? null) { - return self::STATUS_UNINITIALIZED_PARTIAL; - } + if ($initializer = $this->initializer[$k] ?? null) { + $value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); + $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); + $accessor['set']($instance, $propertyName, $value); - $value = $initializer(...[$instance, $propertyName, $propertyScope, LazyObjectRegistry::$defaultProperties[$class][$k] ?? null]); + return $this->status = self::STATUS_INITIALIZED_PARTIAL; + } - $accessor = LazyObjectRegistry::$classAccessors[$propertyScope] ??= LazyObjectRegistry::getClassAccessors($propertyScope); - $accessor['set']($instance, $propertyName, $value); + $status = self::STATUS_UNINITIALIZED_PARTIAL; + + if ($initializer = $this->initializer["\0"] ?? null) { + if (!\is_array($values = $initializer($instance, LazyObjectRegistry::$defaultProperties[$class]))) { + throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values))); + } + $properties = (array) $instance; + foreach ($values as $key => $value) { + if ($k === $key) { + $status = self::STATUS_INITIALIZED_PARTIAL; + } + if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { + $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); + $accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope); + $accessor['set']($instance, $name, $value); + } + } + } - return $this->status = self::STATUS_INITIALIZED_PARTIAL; + return $status; } $this->status = self::STATUS_INITIALIZED_FULL; diff --git a/src/Symfony/Component/VarExporter/LazyGhostTrait.php b/src/Symfony/Component/VarExporter/LazyGhostTrait.php index 46346b6a7b0d7..df40c267fd027 100644 --- a/src/Symfony/Component/VarExporter/LazyGhostTrait.php +++ b/src/Symfony/Component/VarExporter/LazyGhostTrait.php @@ -29,16 +29,23 @@ trait LazyGhostTrait * 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. + * The special "\0" key can be used to define a closure that returns all + * properties at once when full-initialization is needed; it takes the + * instance and its default properties as arguments. * * Properties should be indexed by their array-cast name, see * https://php.net/manual/language.types.array#language.types.array.casting * - * @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 + * @param (\Closure(static):void + * |array + * |array{"\0": \Closure(static, array):array}) $initializer + * @param array|null $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|array $initializer, array $skippedProperties = [], self $instance = null): static + public static function createLazyGhost(\Closure|array $initializer, array $skippedProperties = null, self $instance = null): static { + $onlyProperties = null === $skippedProperties && \is_array($initializer) ? $initializer : null; + if (self::class !== $class = $instance ? $instance::class : static::class) { $skippedProperties["\0".self::class."\0lazyObjectId"] = true; } elseif (\defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) { @@ -48,8 +55,7 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp $instance ??= (Registry::$classReflectors[$class] ??= new \ReflectionClass($class))->newInstanceWithoutConstructor(); 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; + Registry::$states[$id] = new LazyObjectState($initializer, $skippedProperties ??= []); foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) { $reset($instance, $skippedProperties, $onlyProperties); @@ -60,8 +66,10 @@ public static function createLazyGhost(\Closure|array $initializer, array $skipp /** * Returns whether the object is initialized. + * + * @param $partial Whether partially initialized objects should be considered as initialized */ - public function isLazyObjectInitialized(): bool + public function isLazyObjectInitialized(bool $partial = false): bool { if (!$state = Registry::$states[$this->lazyObjectId ?? ''] ?? null) { return true; @@ -73,6 +81,11 @@ public function isLazyObjectInitialized(): bool $class = $this::class; $properties = (array) $this; + + if ($partial) { + return (bool) array_intersect_key($state->initializer, $properties); + } + $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); foreach ($state->initializer as $key => $initializer) { if (!\array_key_exists($key, $properties) && isset($propertyScopes[$key])) { @@ -100,6 +113,8 @@ public function initializeLazyObject(): static return $this; } + $values = isset($state->initializer["\0"]) ? null : []; + $class = $this::class; $properties = (array) $this; $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class); @@ -107,9 +122,25 @@ public function initializeLazyObject(): static if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) { continue; } + $scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class); - $state->initialize($this, $name, $readonlyScope ?? ('*' !== $scope ? $scope : null)); - $properties = (array) $this; + if (null === $values) { + if (!\is_array($values = ($state->initializer["\0"])($this, Registry::$defaultProperties[$class]))) { + throw new \TypeError(sprintf('The lazy-initializer defined for instance of "%s" must return an array, got "%s".', $class, get_debug_type($values))); + } + + if (\array_key_exists($key, $properties = (array) $this)) { + continue; + } + } + + if (\array_key_exists($key, $values)) { + $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope); + $accessor['set']($this, $name, $properties[$key] = $values[$key]); + } else { + $state->initialize($this, $name, $scope); + $properties = (array) $this; + } } return $this; diff --git a/src/Symfony/Component/VarExporter/LazyObjectInterface.php b/src/Symfony/Component/VarExporter/LazyObjectInterface.php index 314ba85e37039..36708845912ca 100644 --- a/src/Symfony/Component/VarExporter/LazyObjectInterface.php +++ b/src/Symfony/Component/VarExporter/LazyObjectInterface.php @@ -15,8 +15,10 @@ interface LazyObjectInterface { /** * Returns whether the object is initialized. + * + * @param $partial Whether partially initialized objects should be considered as initialized */ - public function isLazyObjectInitialized(): bool; + public function isLazyObjectInitialized(bool $partial = false): bool; /** * Forces initialization of a lazy object and returns it. diff --git a/src/Symfony/Component/VarExporter/LazyProxyTrait.php b/src/Symfony/Component/VarExporter/LazyProxyTrait.php index d79806c086d5a..97509eb35321b 100644 --- a/src/Symfony/Component/VarExporter/LazyProxyTrait.php +++ b/src/Symfony/Component/VarExporter/LazyProxyTrait.php @@ -47,8 +47,10 @@ public static function createLazyProxy(\Closure $initializer, self $instance = n /** * Returns whether the object is initialized. + * + * @param $partial Whether partially initialized objects should be considered as initialized */ - public function isLazyObjectInitialized(): bool + public function isLazyObjectInitialized(bool $partial = false): bool { if (0 >= ($this->lazyObjectId ?? 0)) { return true; diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 44ef1f17eede9..3663217435268 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -248,6 +248,7 @@ public function testPartialInitialization() $this->assertFalse($instance->isLazyObjectInitialized()); $this->assertSame(123, $instance->public); $this->assertFalse($instance->isLazyObjectInitialized()); + $this->assertTrue($instance->isLazyObjectInitialized(true)); $this->assertSame(['public', "\0".TestClass::class."\0lazyObjectId"], array_keys((array) $instance)); $this->assertSame(1, $counter); @@ -330,4 +331,89 @@ public function testReflectionPropertyGetValue() $this->assertSame(-3, $r->getValue($obj)); } + + public function testFullPartialInitialization() + { + $counter = 0; + $initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + return 234; + }; + $instance = ChildTestClass::createLazyGhost([ + 'public' => $initializer, + 'publicReadonly' => $initializer, + "\0*\0protected" => $initializer, + "\0" => function ($obj, $defaults) use (&$instance, &$counter) { + $counter += 1000; + $this->assertSame($instance, $obj); + + return [ + 'public' => 345, + 'publicReadonly' => 456, + "\0*\0protected" => 567, + ] + $defaults; + }, + ]); + + $this->assertSame($instance, $instance->initializeLazyObject()); + $this->assertSame(345, $instance->public); + $this->assertSame(456, $instance->publicReadonly); + $this->assertSame(6, ((array) $instance)["\0".ChildTestClass::class."\0private"]); + $this->assertSame(3, ((array) $instance)["\0".TestClass::class."\0private"]); + $this->assertSame(1000, $counter); + } + + public function testPartialInitializationFallback() + { + $counter = 0; + $instance = ChildTestClass::createLazyGhost([ + "\0" => function ($obj) use (&$instance, &$counter) { + $counter += 1000; + $this->assertSame($instance, $obj); + + return [ + 'public' => 345, + 'publicReadonly' => 456, + "\0*\0protected" => 567, + ]; + }, + ], []); + + $this->assertSame(345, $instance->public); + $this->assertSame(456, $instance->publicReadonly); + $this->assertSame(567, ((array) $instance)["\0*\0protected"]); + $this->assertSame(1000, $counter); + } + + public function testFullInitializationAfterPartialInitialization() + { + $counter = 0; + $initializer = static function (ChildTestClass $instance, string $property, ?string $scope, mixed $default) use (&$counter) { + ++$counter; + + return 234; + }; + $instance = ChildTestClass::createLazyGhost([ + 'public' => $initializer, + 'publicReadonly' => $initializer, + "\0*\0protected" => $initializer, + "\0" => function ($obj, $defaults) use (&$instance, &$counter) { + $counter += 1000; + $this->assertSame($instance, $obj); + + return [ + 'public' => 345, + 'publicReadonly' => 456, + "\0*\0protected" => 567, + ] + $defaults; + }, + ]); + + $this->assertSame(234, $instance->public); + $this->assertSame($instance, $instance->initializeLazyObject()); + $this->assertSame(234, $instance->public); + $this->assertSame(456, $instance->publicReadonly); + $this->assertSame(6, ((array) $instance)["\0".ChildTestClass::class."\0private"]); + $this->assertSame(3, ((array) $instance)["\0".TestClass::class."\0private"]); + $this->assertSame(1001, $counter); + } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy