Skip to content

[VarExporter] Add trait to help implement lazy loading ghost objects #46751

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
Jul 12, 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
11 changes: 11 additions & 0 deletions .github/expected-missing-return-types.diff
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,17 @@ index f7ef22df5c..9439e9526f 100644
+ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic): array
{
$refs = $values;
diff --git a/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php b/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php
index 471c1a6b91..2e19d2ab2d 100644
--- a/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php
+++ b/src/Symfony/Component/VarExporter/Internal/GhostObjectState.php
@@ -54,5 +54,5 @@ class GhostObjectState
* @return bool Returns true when fully-initializing, false when partial-initializing
*/
- public function initialize($instance, $propertyName, $propertyScope)
+ public function initialize($instance, $propertyName, $propertyScope): bool
{
if (!$this->status) {
diff --git a/src/Symfony/Component/Workflow/Event/Event.php b/src/Symfony/Component/Workflow/Event/Event.php
index cd7fab7896..b340eba38e 100644
--- a/src/Symfony/Component/Workflow/Event/Event.php
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/VarExporter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
6.2
---

* Add `LazyGhostObjectTrait`
* Add `Hydrator::hydrate()`
* Preserve PHP references also when using `Hydrator::hydrate()` or `Instantiator::instantiate()`
* Add support for hydrating from native (array) casts
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/VarExporter/Instantiator.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ public static function instantiate(string $class, array $properties = [], array
$instance = unserialize('O:'.\strlen($class).':"'.$class.'":0:{}');
}

return Hydrator::hydrate($instance, $properties, $scopedProperties);
return $properties || $scopedProperties ? Hydrator::hydrate($instance, $properties, $scopedProperties) : $instance;
}
}
19 changes: 19 additions & 0 deletions src/Symfony/Component/VarExporter/Internal/EmptyScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\VarExporter\Internal;

/**
* @internal
*/
enum EmptyScope
{
}
126 changes: 126 additions & 0 deletions src/Symfony/Component/VarExporter/Internal/GhostObjectRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\VarExporter\Internal;

/**
* Stores the state of lazy ghost objects and caches related reflection information.
*
* As a micro-optimization, this class uses no type declarations.
*
* @internal
*/
class GhostObjectRegistry
{
/**
* @var array<int, GhostObjectState>
*/
public static $states = [];

/**
* @var array<class-string, \ReflectionClass>
*/
public static $classReflectors = [];

/**
* @var array<class-string, array<string, mixed>>
*/
public static $defaultProperties = [];

/**
* @var array<class-string, list<\Closure>>
*/
public static $classResetters = [];

/**
* @var array<class-string, array{get: \Closure, set: \Closure, isset: \Closure, unset: \Closure}>
*/
public static $classAccessors = [];

/**
* @var array<class-string, array{get: int, set: bool, isset: bool, unset: bool, clone: bool, serialize: bool, sleep: bool, destruct: bool}>
*/
public static $parentMethods = [];

public static function getClassResetters($class)
{
$classProperties = [];
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);

foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) {
if ('lazyGhostObjectId' !== $name && null !== ($propertyScopes["\0$scope\0$name"] ?? $propertyScopes["\0*\0$name"] ?? $readonlyScope)) {
$classProperties[$readonlyScope ?? $scope][$name] = $key;
}
}

$resetters = [];
foreach ($classProperties as $scope => $properties) {
$resetters[] = \Closure::bind(static function ($instance, $skippedProperties = []) use ($properties) {
foreach ($properties as $name => $key) {
if (!\array_key_exists($key, $skippedProperties)) {
unset($instance->$name);
}
}
}, null, $scope);
}

$resetters[] = static function ($instance, $skippedProperties = []) {
foreach ((array) $instance as $name => $value) {
if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties)) {
unset($instance->$name);
}
}
};

return $resetters;
}

public static function getClassAccessors($class)
{
return \Closure::bind(static function () {
return [
'get' => static function &($instance, $name, $readonly) {
if (!$readonly) {
return $instance->$name;
}
$value = $instance->$name;

return $value;
},
'set' => static function ($instance, $name, $value) {
$instance->$name = $value;
},
'isset' => static function ($instance, $name) {
return isset($instance->$name);
},
'unset' => static function ($instance, $name) {
unset($instance->$name);
},
];
}, null, $class)();
}

public static function getParentMethods($class)
{
$parent = get_parent_class($class);

return [
'get' => $parent && method_exists($parent, '__get') ? ((new \ReflectionMethod($parent, '__get'))->returnsReference() ? 2 : 1) : 0,
'set' => $parent && method_exists($parent, '__set'),
'isset' => $parent && method_exists($parent, '__isset'),
'unset' => $parent && method_exists($parent, '__unset'),
'clone' => $parent && method_exists($parent, '__clone'),
'serialize' => $parent && method_exists($parent, '__serialize'),
'sleep' => $parent && method_exists($parent, '__sleep'),
'destruct' => $parent && method_exists($parent, '__destruct'),
];
}
}
87 changes: 87 additions & 0 deletions src/Symfony/Component/VarExporter/Internal/GhostObjectState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\VarExporter\Hydrator;

/**
* Keeps the state of lazy ghost objects.
*
* As a micro-optimization, this class uses no type declarations.
*
* @internal
*/
class GhostObjectState
{
public const STATUS_INITIALIZED_PARTIAL = 1;
public const STATUS_UNINITIALIZED_FULL = 2;
public const STATUS_INITIALIZED_FULL = 3;

public \Closure $initializer;

/**
* @var array<class-string|'*', array<string, true>>
*/
public $preInitUnsetProperties;

/**
* @var array<string, true>
*/
public $preInitSetProperties = [];

/**
* @var array<class-string|'*', array<string, true>>
*/
public $unsetProperties = [];

/**
* One of self::STATUS_*.
*
* @var int
*/
public $status;

/**
* @return bool Returns true when fully-initializing, false when partial-initializing
*/
public function initialize($instance, $propertyName, $propertyScope)
{
if (!$this->status) {
$this->status = 1 < (new \ReflectionFunction($this->initializer))->getNumberOfRequiredParameters() ? self::STATUS_INITIALIZED_PARTIAL : self::STATUS_UNINITIALIZED_FULL;
$this->preInitUnsetProperties ??= $this->unsetProperties;
}

if (self::STATUS_INITIALIZED_FULL === $this->status) {
return true;
}

if (self::STATUS_UNINITIALIZED_FULL === $this->status) {
if ($defaultProperties = array_diff_key(GhostObjectRegistry::$defaultProperties[\get_class($instance)], (array) $instance)) {
Hydrator::hydrate($instance, $defaultProperties);
}

$this->status = self::STATUS_INITIALIZED_FULL;
($this->initializer)($instance);

return true;
}

$value = ($this->initializer)(...[$instance, $propertyName, $propertyScope]);

$propertyScope ??= \get_class($instance);
$accessor = GhostObjectRegistry::$classAccessors[$propertyScope] ??= GhostObjectRegistry::getClassAccessors($propertyScope);

$accessor['set']($instance, $propertyName, $value);

return false;
}
}
25 changes: 25 additions & 0 deletions src/Symfony/Component/VarExporter/LazyGhostObjectInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\VarExporter;

interface LazyGhostObjectInterface
{
/**
* Forces initialization of a lazy ghost object.
*/
public function initializeLazyGhostObject(): void;

/**
* @return bool Returns false when the object cannot be reset, ie when it's not a ghost object
*/
public function resetLazyGhostObject(): bool;
}
Loading