update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-18 20:32:18 +07:00
parent 4554035227
commit dcaf267458
3359 changed files with 153185 additions and 205489 deletions

View File

@@ -1,13 +1,6 @@
CHANGELOG
=========
8.0
---
* Restrict `ProxyHelper::generateLazyProxy()` to generating abstraction-based lazy decorators; use native lazy proxies otherwise
* Remove `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
* Remove `ProxyHelper::generateLazyGhost()`, use native lazy objects instead
7.4
---

View File

@@ -145,7 +145,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
$p = $parent->hasProperty($n) ? $parent->getProperty($n) : null;
} while (!$p && $parent = $parent->getParentClass());
$c = $p && (!$p->isPublic() || $p->isProtectedSet() || $p->isPrivateSet()) ? $p->class : 'stdClass';
$c = $p && (!$p->isPublic() || (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly())) ? $p->class : 'stdClass';
} elseif ('*' === $n[1]) {
$n = substr($n, 3);
$c = $reflector->getProperty($n)->class;

View File

@@ -222,7 +222,7 @@ public static function getSimpleHydrator($class)
if ($propertyReflector->isStatic()) {
continue;
}
if (!$propertyReflector->isAbstract() && $propertyReflector->getHooks()) {
if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) {
$notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...);
} elseif ($propertyReflector->isReadOnly()) {
$notByRef->{$propertyReflector->name} = static function ($object, $value) use ($propertyReflector) {
@@ -277,7 +277,7 @@ public static function getPropertyScopes($class): array
$name = $property->name;
$access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0);
if (!$property->isAbstract() && $h = $property->getHooks()) {
if (\PHP_VERSION_ID >= 80400 && !$property->isAbstract() && $h = $property->getHooks()) {
$access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0);
}
@@ -289,7 +289,7 @@ public static function getPropertyScopes($class): array
$propertyScopes[$name] = [$class, $name, null, $access, $property];
if ($flags & \ReflectionProperty::IS_PRIVATE_SET) {
if ($flags & (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY)) {
$propertyScopes[$name][2] = $property->class;
}
@@ -310,7 +310,7 @@ public static function getPropertyScopes($class): array
$name = $property->name;
$access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0);
if ($h = $property->getHooks()) {
if (\PHP_VERSION_ID >= 80400 && $h = $property->getHooks()) {
$access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0);
}

View File

@@ -95,7 +95,7 @@ public function &__get($name): mixed
$notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF || ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET;
}
if ($notByRef || 2 !== ((Registry::$parentGet[$class] ??= Registry::getParentGet($class)) ?: 2)) {
if ($notByRef || 2 !== ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['get'] ?: 2)) {
$value = $instance->$name;
return $value;

View File

@@ -41,9 +41,11 @@ class LazyObjectRegistry
public static array $classAccessors = [];
/**
* @var array<class-string, int>
* @var array<class-string, array{set: bool, isset: bool, unset: bool, clone: bool, serialize: bool, unserialize: bool, sleep: bool, wakeup: bool, destruct: bool, get: int}>
*/
public static array $parentGet = [];
public static array $parentMethods = [];
public static ?\Closure $noInitializerState = null;
public static function getClassResetters($class)
{
@@ -84,16 +86,80 @@ public static function getClassResetters($class)
return $resetters;
}
public static function getParentGet($class): int
public static function getClassAccessors($class)
{
return \Closure::bind(static fn () => [
'get' => static function &($instance, $name, $notByRef) {
if (!$notByRef) {
return $instance->$name;
}
$value = $instance->$name;
return $value;
},
'set' => static function ($instance, $name, $value) {
$instance->$name = $value;
},
'isset' => static fn ($instance, $name) => isset($instance->$name),
'unset' => static function ($instance, $name) {
unset($instance->$name);
},
], null, \Closure::class === $class ? null : $class)();
}
public static function getParentMethods($class)
{
$parent = get_parent_class($class);
$methods = [];
if (!$parent || !method_exists($parent, '__get')) {
return 0;
foreach (['set', 'isset', 'unset', 'clone', 'serialize', 'unserialize', 'sleep', 'wakeup', 'destruct', 'get'] as $method) {
if (!$parent || !method_exists($parent, '__'.$method)) {
$methods[$method] = false;
} else {
$m = new \ReflectionMethod($parent, '__'.$method);
$methods[$method] = !$m->isAbstract() && !$m->isPrivate();
}
}
$m = new \ReflectionMethod($parent, '__get');
$methods['get'] = $methods['get'] ? ($m->returnsReference() ? 2 : 1) : 0;
return !$m->isAbstract() && !$m->isPrivate() ? ($m->returnsReference() ? 2 : 1) : 0;
return $methods;
}
public static function getScopeForRead($propertyScopes, $class, $property)
{
if (!isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) {
return null;
}
$frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2];
if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) {
$scope = $frame['object']->class;
}
if ('*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) {
return null;
}
return $scope;
}
public static function getScopeForWrite($propertyScopes, $class, $property, $flags)
{
if (!($flags & (\ReflectionProperty::IS_PRIVATE | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_READONLY | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET | \ReflectionProperty::IS_PROTECTED_SET : 0)))) {
return null;
}
$frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2];
if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) {
$scope = $frame['object']->class;
}
if ($flags & (\ReflectionProperty::IS_PRIVATE | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY))) {
return $scope;
}
if ($flags & (\ReflectionProperty::IS_PROTECTED | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PROTECTED_SET : 0)) && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) {
return null;
}
return $scope;
}
}

View File

@@ -11,6 +11,8 @@
namespace Symfony\Component\VarExporter\Internal;
use Symfony\Component\VarExporter\Hydrator as PublicHydrator;
/**
* Keeps the state of lazy objects.
*
@@ -20,10 +22,80 @@
*/
class LazyObjectState
{
public ?\Closure $initializer = null;
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 self::STATUS_*
*/
public int $status = self::STATUS_UNINITIALIZED_FULL;
public object $realInstance;
public object $cloneInstance;
/**
* @param array<string, true> $skippedProperties
*/
public function __construct(
public ?\Closure $initializer = null,
public array $skippedProperties = [],
) {
}
public function initialize($instance, $propertyName, $writeScope)
{
if (self::STATUS_UNINITIALIZED_FULL !== $this->status) {
return $this->status;
}
$this->status = self::STATUS_INITIALIZED_PARTIAL;
try {
if ($defaultProperties = array_diff_key(LazyObjectRegistry::$defaultProperties[$instance::class], $this->skippedProperties)) {
PublicHydrator::hydrate($instance, $defaultProperties);
}
($this->initializer)($instance);
} catch (\Throwable $e) {
$this->status = self::STATUS_UNINITIALIZED_FULL;
$this->reset($instance);
throw $e;
}
return $this->status = self::STATUS_INITIALIZED_FULL;
}
public function reset($instance): void
{
$class = $instance::class;
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
$skippedProperties = $this->skippedProperties;
$properties = (array) $instance;
foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
if ($k === $key && ($access & Hydrator::PROPERTY_HAS_HOOKS || ($access >> 2) & \ReflectionProperty::IS_READONLY || !\array_key_exists($k, $properties))) {
$skippedProperties[$k] = true;
}
}
foreach (LazyObjectRegistry::$classResetters[$class] as $reset) {
$reset($instance, $skippedProperties);
}
foreach ((array) $instance as $name => $value) {
if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties)) {
unset($instance->$name);
}
}
$this->status = self::STATUS_UNINITIALIZED_FULL;
}
public function __clone()
{
if (isset($this->cloneInstance)) {

View File

@@ -0,0 +1,34 @@
<?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\Serializer\Attribute\Ignore;
if (\PHP_VERSION_ID >= 80300) {
/**
* @internal
*/
trait LazyObjectTrait
{
#[Ignore]
private readonly LazyObjectState $lazyObjectState;
}
} else {
/**
* @internal
*/
trait LazyObjectTrait
{
#[Ignore]
private LazyObjectState $lazyObjectState;
}
}

View File

@@ -0,0 +1,376 @@
<?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;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
use Symfony\Component\VarExporter\Internal\LazyObjectState;
use Symfony\Component\VarExporter\Internal\LazyObjectTrait;
if (\PHP_VERSION_ID >= 80400) {
trigger_deprecation('symfony/var-exporter', '7.3', 'The "%s" trait is deprecated, use native lazy objects instead.', LazyGhostTrait::class);
}
/**
* @deprecated since Symfony 7.3, use native lazy objects instead
*/
trait LazyGhostTrait
{
use LazyObjectTrait;
/**
* Creates a lazy-loading ghost instance.
*
* Skipped properties should be indexed by their array-cast identifier, see
* https://php.net/manual/language.types.array#language.types.array.casting
*
* @param \Closure(static):void $initializer The closure should initialize the object it receives as argument
* @param array<string, true>|null $skippedProperties An array indexed by the properties to skip, a.k.a. the ones
* that the initializer doesn't initialize, if any
* @param static|null $instance
*/
public static function createLazyGhost(\Closure $initializer, ?array $skippedProperties = null, ?object $instance = null): static
{
if (self::class !== $class = $instance ? $instance::class : static::class) {
$skippedProperties["\0".self::class."\0lazyObjectState"] = true;
}
if (!isset(Registry::$defaultProperties[$class])) {
Registry::$classReflectors[$class] ??= new \ReflectionClass($class);
$instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
Registry::$defaultProperties[$class] ??= (array) $instance;
Registry::$classResetters[$class] ??= Registry::getClassResetters($class);
if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
}
} else {
$instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
}
if (isset($instance->lazyObjectState)) {
$instance->lazyObjectState->initializer = $initializer;
$instance->lazyObjectState->skippedProperties = $skippedProperties ??= [];
if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $instance->lazyObjectState->status) {
$instance->lazyObjectState->reset($instance);
}
return $instance;
}
$instance->lazyObjectState = new LazyObjectState($initializer, $skippedProperties ??= []);
foreach (Registry::$classResetters[$class] as $reset) {
$reset($instance, $skippedProperties);
}
return $instance;
}
/**
* Returns whether the object is initialized.
*
* @param bool $partial Whether partially initialized objects should be considered as initialized
*/
#[Ignore]
public function isLazyObjectInitialized(bool $partial = false): bool
{
if (!$state = $this->lazyObjectState ?? null) {
return true;
}
return LazyObjectState::STATUS_INITIALIZED_FULL === $state->status;
}
/**
* Forces initialization of a lazy object and returns it.
*/
public function initializeLazyObject(): static
{
if (!$state = $this->lazyObjectState ?? null) {
return $this;
}
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
$state->initialize($this, '', null);
}
return $this;
}
/**
* @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
*/
public function resetLazyObject(): bool
{
if (!$state = $this->lazyObjectState ?? null) {
return false;
}
if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $state->status) {
$state->reset($this);
}
return true;
}
public function &__get($name): mixed
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
$notByRef = 0;
if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForRead($propertyScopes, $class, $name);
$state = $this->lazyObjectState ?? null;
if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) {
$notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF;
if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) {
// Work around php/php-src#12695
$property = null === $scope ? $name : "\0$scope\0$name";
$property = $propertyScopes[$property][4]
?? Hydrator::$propertyScopes[$this::class][$property][4] = new \ReflectionProperty($scope ?? $class, $name);
} else {
$property = null;
}
if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) {
$scope ??= $writeScope;
}
if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) {
goto get_in_scope;
}
}
}
if ($parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get']) {
if (2 === $parent) {
return parent::__get($name);
}
$value = parent::__get($name);
return $value;
}
if (null === $class) {
$frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
trigger_error(\sprintf('Undefined property: %s::$%s in %s on line %s', $this::class, $name, $frame['file'], $frame['line']), \E_USER_NOTICE);
}
get_in_scope:
try {
if (null === $scope) {
if (!$notByRef) {
return $this->$name;
}
$value = $this->$name;
return $value;
}
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
return $accessor['get']($this, $name, $notByRef);
} catch (\Error $e) {
if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
throw $e;
}
try {
if (null === $scope) {
$this->$name = [];
return $this->$name;
}
$accessor['set']($this, $name, []);
return $accessor['get']($this, $name, $notByRef);
} catch (\Error) {
if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) {
throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious());
}
throw $e;
}
}
}
public function __set($name, $value): void
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
$state = $this->lazyObjectState ?? null;
if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
) {
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
$state->initialize($this, $name, $writeScope ?? $scope);
}
goto set_in_scope;
}
}
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['set']) {
parent::__set($name, $value);
return;
}
set_in_scope:
if (null === $scope) {
$this->$name = $value;
} else {
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
$accessor['set']($this, $name, $value);
}
}
public function __isset($name): bool
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForRead($propertyScopes, $class, $name);
$state = $this->lazyObjectState ?? null;
if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
&& LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)
) {
goto isset_in_scope;
}
}
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['isset']) {
return parent::__isset($name);
}
isset_in_scope:
if (null === $scope) {
return isset($this->$name);
}
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
return $accessor['isset']($this, $name);
}
public function __unset($name): void
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
$state = $this->lazyObjectState ?? null;
if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
) {
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
$state->initialize($this, $name, $writeScope ?? $scope);
}
goto unset_in_scope;
}
}
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['unset']) {
parent::__unset($name);
return;
}
unset_in_scope:
if (null === $scope) {
unset($this->$name);
} else {
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
$accessor['unset']($this, $name);
}
}
public function __clone(): void
{
if ($state = $this->lazyObjectState ?? null) {
$this->lazyObjectState = clone $state;
}
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) {
parent::__clone();
}
}
public function __serialize(): array
{
$class = self::class;
if ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) {
$properties = parent::__serialize();
} else {
$this->initializeLazyObject();
$properties = (array) $this;
}
unset($properties["\0$class\0lazyObjectState"]);
if (Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) {
return $properties;
}
$scope = get_parent_class($class);
$data = [];
foreach (parent::__sleep() as $name) {
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null;
if (null === $k) {
trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE);
} else {
$data[$k] = $value;
}
}
return $data;
}
public function __destruct()
{
$state = $this->lazyObjectState ?? null;
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state?->status) {
return;
}
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) {
parent::__destruct();
}
}
#[Ignore]
private function setLazyObjectAsInitialized(bool $initialized): void
{
if ($state = $this->lazyObjectState ?? null) {
$state->status = $initialized ? LazyObjectState::STATUS_INITIALIZED_FULL : LazyObjectState::STATUS_UNINITIALIZED_FULL;
}
}
}

View File

@@ -0,0 +1,376 @@
<?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;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\VarExporter\Hydrator as PublicHydrator;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
use Symfony\Component\VarExporter\Internal\LazyObjectState;
use Symfony\Component\VarExporter\Internal\LazyObjectTrait;
if (\PHP_VERSION_ID >= 80400) {
trigger_deprecation('symfony/var-exporter', '7.3', 'The "%s" trait is deprecated, use native lazy objects instead.', LazyProxyTrait::class);
}
/**
* @deprecated since Symfony 7.3, use native lazy objects instead
*/
trait LazyProxyTrait
{
use LazyObjectTrait;
/**
* Creates a lazy-loading virtual proxy.
*
* @param \Closure():object $initializer Returns the proxied object
* @param static|null $instance
*/
public static function createLazyProxy(\Closure $initializer, ?object $instance = null): static
{
if (self::class !== $class = $instance ? $instance::class : static::class) {
$skippedProperties = ["\0".self::class."\0lazyObjectState" => true];
}
if (!isset(Registry::$defaultProperties[$class])) {
Registry::$classReflectors[$class] ??= new \ReflectionClass($class);
$instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
Registry::$defaultProperties[$class] ??= (array) $instance;
if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
}
Registry::$classResetters[$class] ??= Registry::getClassResetters($class);
} else {
$instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
}
if (isset($instance->lazyObjectState)) {
$instance->lazyObjectState->initializer = $initializer;
unset($instance->lazyObjectState->realInstance);
return $instance;
}
$instance->lazyObjectState = new LazyObjectState($initializer);
foreach (Registry::$classResetters[$class] as $reset) {
$reset($instance, $skippedProperties ??= []);
}
return $instance;
}
/**
* Returns whether the object is initialized.
*
* @param bool $partial Whether partially initialized objects should be considered as initialized
*/
#[Ignore]
public function isLazyObjectInitialized(bool $partial = false): bool
{
return !isset($this->lazyObjectState) || isset($this->lazyObjectState->realInstance) || Registry::$noInitializerState === $this->lazyObjectState->initializer;
}
/**
* Forces initialization of a lazy object and returns it.
*/
public function initializeLazyObject(): parent
{
if ($state = $this->lazyObjectState ?? null) {
return $state->realInstance ??= ($state->initializer)();
}
return $this;
}
/**
* @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
*/
public function resetLazyObject(): bool
{
if (!isset($this->lazyObjectState) || Registry::$noInitializerState === $this->lazyObjectState->initializer) {
return false;
}
unset($this->lazyObjectState->realInstance);
return true;
}
public function &__get($name): mixed
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
$instance = $this;
$notByRef = 0;
if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
$notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF;
$scope = Registry::getScopeForRead($propertyScopes, $class, $name);
if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) {
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
}
if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) {
$scope ??= $writeScope;
}
$parent = 2;
goto get_in_scope;
}
}
$parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get'];
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
} else {
if (2 === $parent) {
return parent::__get($name);
}
$value = parent::__get($name);
return $value;
}
if (!$parent && null === $class && !\array_key_exists($name, (array) $instance)) {
$frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
trigger_error(\sprintf('Undefined property: %s::$%s in %s on line %s', $instance::class, $name, $frame['file'], $frame['line']), \E_USER_NOTICE);
}
get_in_scope:
$notByRef = $notByRef || 1 === $parent;
try {
if (null === $scope) {
if (!$notByRef) {
return $instance->$name;
}
$value = $instance->$name;
return $value;
}
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
return $accessor['get']($instance, $name, $notByRef);
} catch (\Error $e) {
if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
throw $e;
}
try {
if (null === $scope) {
$instance->$name = [];
return $instance->$name;
}
$accessor['set']($instance, $name, []);
return $accessor['get']($instance, $name, $notByRef);
} catch (\Error) {
throw $e;
}
}
}
public function __set($name, $value): void
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
$instance = $this;
if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
}
goto set_in_scope;
}
}
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
} elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['set']) {
parent::__set($name, $value);
return;
}
set_in_scope:
if (null === $scope) {
$instance->$name = $value;
} else {
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
$accessor['set']($instance, $name, $value);
}
}
public function __isset($name): bool
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
$instance = $this;
if ([$class] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForRead($propertyScopes, $class, $name);
if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) {
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
}
goto isset_in_scope;
}
}
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
} elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['isset']) {
return parent::__isset($name);
}
isset_in_scope:
if (null === $scope) {
return isset($instance->$name);
}
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
return $accessor['isset']($instance, $name);
}
public function __unset($name): void
{
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
$scope = null;
$instance = $this;
if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
$scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
}
goto unset_in_scope;
}
}
if ($state = $this->lazyObjectState ?? null) {
$instance = $state->realInstance ??= ($state->initializer)();
} elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['unset']) {
parent::__unset($name);
return;
}
unset_in_scope:
if (null === $scope) {
unset($instance->$name);
} else {
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
$accessor['unset']($instance, $name);
}
}
public function __clone(): void
{
if (!isset($this->lazyObjectState)) {
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) {
parent::__clone();
}
return;
}
$this->lazyObjectState = clone $this->lazyObjectState;
}
public function __serialize(): array
{
$class = self::class;
$state = $this->lazyObjectState ?? null;
if (!$state && (Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) {
$properties = parent::__serialize();
} else {
$properties = (array) $this;
if ($state) {
unset($properties["\0$class\0lazyObjectState"]);
$properties["\0$class\0lazyObjectReal"] = $state->realInstance ??= ($state->initializer)();
}
}
if ($state || Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) {
return $properties;
}
$scope = get_parent_class($class);
$data = [];
foreach (parent::__sleep() as $name) {
$value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null;
if (null === $k) {
trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE);
} else {
$data[$k] = $value;
}
}
return $data;
}
public function __unserialize(array $data): void
{
$class = self::class;
if ($instance = $data["\0$class\0lazyObjectReal"] ?? null) {
unset($data["\0$class\0lazyObjectReal"]);
foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
$reset($this, $data);
}
if ($data) {
PublicHydrator::hydrate($this, $data);
}
$this->lazyObjectState = new LazyObjectState(Registry::$noInitializerState ??= static fn () => throw new \LogicException('Lazy proxy has no initializer.'));
$this->lazyObjectState->realInstance = $instance;
} elseif ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['unserialize']) {
parent::__unserialize($data);
} else {
PublicHydrator::hydrate($this, $data);
if (Registry::$parentMethods[$class]['wakeup']) {
parent::__wakeup();
}
}
}
public function __destruct()
{
if (isset($this->lazyObjectState)) {
return;
}
if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) {
parent::__destruct();
}
}
}

View File

@@ -21,6 +21,108 @@
*/
final class ProxyHelper
{
/**
* Helps generate lazy-loading ghost objects.
*
* @deprecated since Symfony 7.3, use native lazy objects instead
*
* @throws LogicException When the class is incompatible with ghost objects
*/
public static function generateLazyGhost(\ReflectionClass $class): string
{
if (\PHP_VERSION_ID >= 80400) {
trigger_deprecation('symfony/var-exporter', '7.3', 'Using ProxyHelper::generateLazyGhost() is deprecated, use native lazy objects instead.');
}
if (\PHP_VERSION_ID < 80300 && $class->isReadOnly()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost with PHP < 8.3: class "%s" is readonly.', $class->name));
}
if ($class->isFinal()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name));
}
if ($class->isInterface() || $class->isAbstract() || $class->isTrait()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
}
if (\stdClass::class !== $class->name && $class->isInternal()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" is internal.', $class->name));
}
if ($class->hasMethod('__get') && 'mixed' !== (self::exportType($class->getMethod('__get')) ?? 'mixed')) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: return type of method "%s::__get()" should be "mixed".', $class->name));
}
static $traitMethods;
$traitMethods ??= (new \ReflectionClass(LazyGhostTrait::class))->getMethods();
foreach ($traitMethods as $method) {
if ($class->hasMethod($method->name) && $class->getMethod($method->name)->isFinal()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: method "%s::%s()" is final.', $class->name, $method->name));
}
}
$parent = $class;
while ($parent = $parent->getParentClass()) {
if (\stdClass::class !== $parent->name && $parent->isInternal()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name));
}
}
$hooks = '';
$propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name);
foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
$flags = $access >> 2;
if ($k !== $key || !($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) {
continue;
}
if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: property "%s::$%s" is final or private(set).', $class->name, $name));
}
$p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
$type = self::exportType($p);
$hooks .= "\n "
.($p->isProtected() ? 'protected' : 'public')
.($p->isProtectedSet() ? ' protected(set)' : '')
." {$type} \${$name}"
.($p->hasDefaultValue() ? ' = '.VarExporter::export($p->getDefaultValue()) : '')
." {\n";
foreach ($p->getHooks() as $hook => $method) {
if ('get' === $hook) {
$ref = ($method->returnsReference() ? '&' : '');
$hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n";
} elseif ('set' === $hook) {
$parameters = self::exportParameters($method, true);
$arg = '$'.$method->getParameters()[0]->name;
$hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n";
} else {
throw new LogicException(\sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name));
}
}
$hooks .= " }\n";
}
$propertyScopes = self::exportPropertyScopes($class->name, $propertyScopes);
return <<<EOPHP
extends \\{$class->name} implements \Symfony\Component\VarExporter\LazyObjectInterface
{
use \Symfony\Component\VarExporter\LazyGhostTrait;
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
{$hooks}}
// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
EOPHP;
}
/**
* Helps generate lazy-loading decorators.
*
@@ -36,6 +138,9 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
if ($class?->isFinal()) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: class "%s" is final.', $class->name));
}
if (\PHP_VERSION_ID < 80400) {
return self::generateLegacyLazyProxy($class, $interfaces);
}
if ($class && !$class->isAbstract()) {
$parent = $class;
@@ -44,7 +149,8 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
} while (!$extendsInternalClass && $parent = $parent->getParentClass());
if (!$extendsInternalClass) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: leverage native lazy objects instead for class "%s".', $class->name));
trigger_deprecation('symfony/var-exporter', '7.3', 'Generating lazy proxy for class "%s" is deprecated; leverage native lazy objects instead.', $class->name);
// throw new LogicException(\sprintf('Cannot generate lazy proxy: leverage native lazy objects instead for class "%s".', $class->name));
}
}
@@ -270,6 +376,156 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
EOPHP;
}
private static function generateLegacyLazyProxy(?\ReflectionClass $class, array $interfaces): string
{
if (\PHP_VERSION_ID < 80300 && $class?->isReadOnly()) {
throw new LogicException(\sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name));
}
$propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : [];
$methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []];
foreach ($interfaces as $interface) {
if (!$interface->isInterface()) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name));
}
$methodReflectors[] = $interface->getMethods();
}
$extendsInternalClass = false;
if ($parent = $class) {
do {
$extendsInternalClass = \stdClass::class !== $parent->name && $parent->isInternal();
} while (!$extendsInternalClass && $parent = $parent->getParentClass());
}
$methodsHaveToBeProxied = $extendsInternalClass;
$methods = [];
$methodReflectors = array_merge(...$methodReflectors);
foreach ($methodReflectors as $method) {
if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
continue;
}
$methodsHaveToBeProxied = true;
$trait = new \ReflectionMethod(LazyProxyTrait::class, '__get');
$body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
$body[0] = str_replace('): mixed', '): '.$type, $body[0]);
$methods['__get'] = strtr(implode('', $body).' }', [
'Hydrator' => '\\'.Hydrator::class,
'Registry' => '\\'.LazyObjectRegistry::class,
]);
break;
}
foreach ($methodReflectors as $method) {
if (($method->isStatic() && !$method->isAbstract()) || isset($methods[$lcName = strtolower($method->name)])) {
continue;
}
if ($method->isFinal()) {
if ($extendsInternalClass || $methodsHaveToBeProxied || method_exists(LazyProxyTrait::class, $method->name)) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.', $class->name, $method->name));
}
continue;
}
if (method_exists(LazyProxyTrait::class, $method->name) || ($method->isProtected() && !$method->isAbstract())) {
continue;
}
$signature = self::exportSignature($method, true, $args);
$parentCall = $method->isAbstract() ? "throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".')" : "parent::{$method->name}({$args})";
if ($method->isStatic()) {
$body = " $parentCall;";
} elseif (str_ends_with($signature, '): never') || str_ends_with($signature, '): void')) {
$body = <<<EOPHP
if (isset(\$this->lazyObjectState)) {
(\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args});
} else {
{$parentCall};
}
EOPHP;
} else {
if (!$methodsHaveToBeProxied && !$method->isAbstract()) {
// Skip proxying methods that might return $this
foreach (preg_split('/[()|&]++/', self::exportType($method) ?? 'static') as $type) {
if (\in_array($type = ltrim($type, '?'), ['static', 'object'], true)) {
continue 2;
}
foreach ([$class, ...$interfaces] as $r) {
if ($r && is_a($r->name, $type, true)) {
continue 3;
}
}
}
}
$body = <<<EOPHP
if (isset(\$this->lazyObjectState)) {
return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args});
}
return {$parentCall};
EOPHP;
}
$methods[$lcName] = " {$signature}\n {\n{$body}\n }";
}
$types = $interfaces = array_unique(array_column($interfaces, 'name'));
$interfaces[] = LazyObjectInterface::class;
$interfaces = implode(', \\', $interfaces);
$parent = $class ? ' extends \\'.$class->name : '';
array_unshift($types, $class ? 'parent' : '');
$type = ltrim(implode('&\\', $types), '&');
if (!$class) {
$trait = new \ReflectionMethod(LazyProxyTrait::class, 'initializeLazyObject');
$body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
$body[0] = str_replace('): parent', '): '.$type, $body[0]);
$methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods;
}
$body = $methods ? "\n".implode("\n\n", $methods)."\n" : '';
$propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]';
if (
$class?->hasMethod('__unserialize')
&& !$class->getMethod('__unserialize')->getParameters()[0]->getType()
) {
// fix contravariance type problem when $class declares a `__unserialize()` method without typehint.
$lazyProxyTraitStatement = <<<EOPHP
use \Symfony\Component\VarExporter\LazyProxyTrait {
__unserialize as private __doUnserialize;
}
EOPHP;
$body .= <<<EOPHP
public function __unserialize(\$data): void
{
\$this->__doUnserialize(\$data);
}
EOPHP;
} else {
$lazyProxyTraitStatement = <<<EOPHP
use \Symfony\Component\VarExporter\LazyProxyTrait;
EOPHP;
}
return <<<EOPHP
{$parent} implements \\{$interfaces}
{
{$lazyProxyTraitStatement}
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
{$body}}
// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
EOPHP;
}
public static function exportParameters(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
{
$byRefIndex = 0;

View File

@@ -84,6 +84,10 @@
// be called only when and if a *method* is called.
```
In addition, this component provides traits and methods to aid in implementing
the ghost and proxy strategies in previous versions of PHP. Those are deprecated
when using PHP 8.4.
Resources
---------

View File

@@ -16,12 +16,13 @@
}
],
"require": {
"php": ">=8.4"
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/property-access": "^7.4|^8.0",
"symfony/serializer": "^7.4|^8.0",
"symfony/var-dumper": "^7.4|^8.0"
"symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\VarExporter\\": "" },