Skip to content

Commit

Permalink
[VarExporter] Use array<property-name,Closure> for partial initializa…
Browse files Browse the repository at this point in the history
…tion of lazy ghost objects
  • Loading branch information
nicolas-grekas committed Nov 13, 2022
1 parent e6151f9 commit 9fc43ea
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 83 deletions.
8 changes: 4 additions & 4 deletions Internal/LazyObjectRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
31 changes: 16 additions & 15 deletions Internal/LazyObjectState.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, true>
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
}
75 changes: 34 additions & 41 deletions LazyGhostTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, true> $skippedProperties An array indexed by the properties to skip,
* aka the ones that the initializer doesn't set
* @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
*/
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;
Expand All @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -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);
}
Expand All @@ -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;
}

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

Expand All @@ -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;
}
}
Expand Down
4 changes: 2 additions & 2 deletions LazyProxyTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 43 additions & 21 deletions Tests/LazyGhostTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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);
Expand All @@ -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();

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

0 comments on commit 9fc43ea

Please sign in to comment.