Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[VarExporter] Improve partial-initialization API for ghost objects #48312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions src/Symfony/Component/VarExporter/Internal/LazyObjectState.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 40 additions & 9 deletions src/Symfony/Component/VarExporter/LazyGhostTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, \Closure(static, string, ?string, mixed):mixed> $initializer
* @param array<string, true> $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<string, \Closure(static, string, ?string, mixed):mixed>
* |array{"\0": \Closure(static, array<string, mixed>):array<string, mixed>}) $initializer
* @param array<string, true>|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')) {
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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])) {
Expand Down Expand Up @@ -100,16 +113,34 @@ 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);
foreach ($state->initializer as $key => $initializer) {
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;
Expand Down
4 changes: 3 additions & 1 deletion src/Symfony/Component/VarExporter/LazyObjectInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion src/Symfony/Component/VarExporter/LazyProxyTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
86 changes: 86 additions & 0 deletions src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}