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); + } }