initial commit

This commit is contained in:
boris
2025-09-30 09:24:25 +01:00
committed by boris
parent a783a12c97
commit c7770ea03b
4695 changed files with 525784 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
CHANGELOG
=========
7.3
---
* Deprecate using `ProxyHelper::generateLazyProxy()` when native lazy proxies can be used - the method should be used to generate abstraction-based lazy decorators only
* Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
* Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead
7.2
---
* Allow reinitializing lazy objects with a new initializer
6.4
---
* Deprecate per-property lazy-initializers
6.2
---
* Add support for lazy ghost objects and virtual proxies
* Add `Hydrator::hydrate()`
* Preserve PHP references also when using `Hydrator::hydrate()` or `Instantiator::instantiate()`
* Add support for hydrating from native (array) casts
5.1.0
-----
* added argument `array &$foundClasses` to `VarExporter::export()` to ease with preloading exported values
4.2.0
-----
* added the component

View File

@@ -0,0 +1,20 @@
<?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\Exception;
class ClassNotFoundException extends \Exception implements ExceptionInterface
{
public function __construct(string $class, ?\Throwable $previous = null)
{
parent::__construct(\sprintf('Class "%s" not found.', $class), 0, $previous);
}
}

View File

@@ -0,0 +1,16 @@
<?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\Exception;
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,16 @@
<?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\Exception;
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,20 @@
<?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\Exception;
class NotInstantiableTypeException extends \Exception implements ExceptionInterface
{
public function __construct(string $type, ?\Throwable $previous = null)
{
parent::__construct(\sprintf('Type "%s" is not instantiable.', $type), 0, $previous);
}
}

View File

@@ -0,0 +1,78 @@
<?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\VarExporter\Internal\Hydrator as InternalHydrator;
/**
* Utility class to hydrate the properties of an object.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Hydrator
{
/**
* Sets the properties of an object, including private and protected ones.
*
* For example:
*
* // Sets the public or protected $object->propertyName property
* Hydrator::hydrate($object, ['propertyName' => $propertyValue]);
*
* // Sets a private property defined on its parent Bar class:
* Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]);
*
* // Alternative way to set the private $object->privateBarProperty property
* Hydrator::hydrate($object, [], [
* Bar::class => ['privateBarProperty' => $propertyValue],
* ]);
*
* Instances of ArrayObject, ArrayIterator and SplObjectStorage can be hydrated
* by using the special "\0" property name to define their internal value:
*
* // Hydrates an SplObjectStorage where $info1 is attached to $obj1, etc.
* Hydrator::hydrate($object, ["\0" => [$obj1, $info1, $obj2, $info2...]]);
*
* // Hydrates an ArrayObject populated with $inputArray
* Hydrator::hydrate($object, ["\0" => [$inputArray]]);
*
* @template T of object
*
* @param T $instance The object to hydrate
* @param array<string, mixed> $properties The properties to set on the instance
* @param array<class-string, array<string, mixed>> $scopedProperties The properties to set on the instance,
* keyed by their declaring class
*
* @return T
*/
public static function hydrate(object $instance, array $properties = [], array $scopedProperties = []): object
{
if ($properties) {
$class = $instance::class;
$propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class);
foreach ($properties as $name => &$value) {
[$scope, $name, $writeScope] = $propertyScopes[$name] ?? [$class, $name, $class];
$scopedProperties[$writeScope ?? $scope][$name] = &$value;
}
unset($value);
}
foreach ($scopedProperties as $scope => $properties) {
if ($properties) {
(InternalHydrator::$simpleHydrators[$scope] ??= InternalHydrator::getSimpleHydrator($scope))($properties, $instance);
}
}
return $instance;
}
}

View File

@@ -0,0 +1,59 @@
<?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\VarExporter\Exception\ExceptionInterface;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
use Symfony\Component\VarExporter\Internal\Registry;
/**
* A utility class to create objects without calling their constructor.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Instantiator
{
/**
* Creates an object and sets its properties without calling its constructor nor any other methods.
*
* @see Hydrator::hydrate() for examples
*
* @template T of object
*
* @param class-string<T> $class The class of the instance to create
* @param array<string, mixed> $properties The properties to set on the instance
* @param array<class-string, array<string, mixed>> $scopedProperties The properties to set on the instance,
* keyed by their declaring class
*
* @return T
*
* @throws ExceptionInterface When the instance cannot be created
*/
public static function instantiate(string $class, array $properties = [], array $scopedProperties = []): object
{
$reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class);
if (Registry::$cloneable[$class]) {
$instance = clone Registry::$prototypes[$class];
} elseif (Registry::$instantiableWithoutConstructor[$class]) {
$instance = $reflector->newInstanceWithoutConstructor();
} elseif (null === Registry::$prototypes[$class]) {
throw new NotInstantiableTypeException($class);
} elseif ($reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize')) {
$instance = unserialize('C:'.\strlen($class).':"'.$class.'":0:{}');
} else {
$instance = unserialize('O:'.\strlen($class).':"'.$class.'":0:{}');
}
return $properties || $scopedProperties ? Hydrator::hydrate($instance, $properties, $scopedProperties) : $instance;
}
}

View File

@@ -0,0 +1,413 @@
<?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\Exception\NotInstantiableTypeException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class Exporter
{
/**
* Prepares an array of values for VarExporter.
*
* For performance this method is public and has no type-hints.
*
* @param array &$values
* @param \SplObjectStorage $objectsPool
* @param array &$refsPool
* @param int &$objectsCount
* @param bool &$valuesAreStatic
*
* @return array
*
* @throws NotInstantiableTypeException When a value cannot be serialized
*/
public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic)
{
$refs = $values;
foreach ($values as $k => $value) {
if (\is_resource($value)) {
throw new NotInstantiableTypeException(get_resource_type($value).' resource');
}
$refs[$k] = $objectsPool;
if ($isRef = !$valueIsStatic = $values[$k] !== $objectsPool) {
$values[$k] = &$value; // Break hard references to make $values completely
unset($value); // independent from the original structure
$refs[$k] = $value = $values[$k];
if ($value instanceof Reference && 0 > $value->id) {
$valuesAreStatic = false;
++$value->count;
continue;
}
$refsPool[] = [&$refs[$k], $value, &$value];
$refs[$k] = $values[$k] = new Reference(-\count($refsPool), $value);
}
if (\is_array($value)) {
if ($value) {
$value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
}
goto handle_value;
} elseif (!\is_object($value) || $value instanceof \UnitEnum) {
goto handle_value;
}
$valueIsStatic = false;
if (isset($objectsPool[$value])) {
++$objectsCount;
$value = new Reference($objectsPool[$value][0]);
goto handle_value;
}
$class = $value::class;
$reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class);
$properties = [];
$sleep = null;
$proto = Registry::$prototypes[$class];
if ($reflector->hasMethod('__serialize')) {
if (!$reflector->getMethod('__serialize')->isPublic()) {
throw new \Error(\sprintf('Call to %s method "%s::__serialize()".', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class));
}
if (!\is_array($arrayValue = $value->__serialize())) {
throw new \TypeError($class.'::__serialize() must return an array');
}
if ($reflector->hasMethod('__unserialize')) {
$properties = $arrayValue;
goto prepare_value;
}
} elseif (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
// ArrayIterator and ArrayObject need special care because their "flags"
// option changes the behavior of the (array) casting operator.
[$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto);
// populates Registry::$prototypes[$class] with a new instance
Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]);
} elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) {
// By implementing Serializable, SplObjectStorage breaks
// internal references; let's deal with it on our own.
foreach (clone $value as $v) {
$properties[] = $v;
$properties[] = $value[$v];
}
$properties = ['SplObjectStorage' => ["\0" => $properties]];
$arrayValue = (array) $value;
} elseif ($value instanceof \Serializable || $value instanceof \__PHP_Incomplete_Class) {
++$objectsCount;
$objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0];
$value = new Reference($id);
goto handle_value;
} else {
if (method_exists($class, '__sleep')) {
if (!\is_array($sleep = $value->__sleep())) {
trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', \E_USER_NOTICE);
$value = null;
goto handle_value;
}
$sleep = array_flip($sleep);
}
$arrayValue = (array) $value;
}
$proto = (array) $proto;
foreach ($arrayValue as $name => $v) {
$i = 0;
$n = (string) $name;
if ('' === $n || "\0" !== $n[0]) {
$parent = $reflector;
do {
$p = $parent->hasProperty($n) ? $parent->getProperty($n) : null;
} while (!$p && $parent = $parent->getParentClass());
$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;
} else {
$i = strpos($n, "\0", 2);
$c = substr($n, 1, $i - 1);
$n = substr($n, 1 + $i);
}
if (null !== $sleep) {
if (!isset($sleep[$name]) && (!isset($sleep[$n]) || ($i && $c !== $class))) {
unset($arrayValue[$name]);
continue;
}
unset($sleep[$name], $sleep[$n]);
}
if ("\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) {
$properties[$c][$n] = $v;
} elseif (!\array_key_exists($name, $proto) || $proto[$name] !== $v) {
$properties[match ($c) {
'Error' => 'TypeError',
'Exception' => 'ErrorException',
default => $c,
}][$n] = $v;
}
}
if ($sleep) {
foreach ($sleep as $n => $v) {
trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE);
}
}
if (method_exists($class, '__unserialize')) {
$properties = $arrayValue;
}
prepare_value:
$objectsPool[$value] = [$id = \count($objectsPool)];
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
++$objectsCount;
$objectsPool[$value] = [$id, $class, $properties, method_exists($class, '__unserialize') ? -$objectsCount : (method_exists($class, '__wakeup') ? $objectsCount : 0)];
$value = new Reference($id);
handle_value:
if ($isRef) {
unset($value); // Break the hard reference created above
} elseif (!$valueIsStatic) {
$values[$k] = $value;
}
$valuesAreStatic = $valueIsStatic && $valuesAreStatic;
}
return $values;
}
public static function export($value, $indent = '')
{
switch (true) {
case \is_int($value) || \is_float($value): return var_export($value, true);
case [] === $value: return '[]';
case false === $value: return 'false';
case true === $value: return 'true';
case null === $value: return 'null';
case '' === $value: return "''";
case $value instanceof \UnitEnum: return '\\'.ltrim(var_export($value, true), '\\');
}
if ($value instanceof Reference) {
if (0 <= $value->id) {
return '$o['.$value->id.']';
}
if (!$value->count) {
return self::export($value->value, $indent);
}
$value = -$value->id;
return '&$r['.$value.']';
}
$subIndent = $indent.' ';
if (\is_string($value)) {
$code = \sprintf("'%s'", addcslashes($value, "'\\"));
$code = preg_replace_callback("/((?:[\\0\\r\\n]|\u{202A}|\u{202B}|\u{202D}|\u{202E}|\u{2066}|\u{2067}|\u{2068}|\u{202C}|\u{2069})++)(.)/", function ($m) use ($subIndent) {
$m[1] = \sprintf('\'."%s".\'', str_replace(
["\0", "\r", "\n", "\u{202A}", "\u{202B}", "\u{202D}", "\u{202E}", "\u{2066}", "\u{2067}", "\u{2068}", "\u{202C}", "\u{2069}", '\n\\'],
['\0', '\r', '\n', '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}', '\u{2069}', '\n"'."\n".$subIndent.'."\\'],
$m[1]
));
if ("'" === $m[2]) {
return substr($m[1], 0, -2);
}
if (str_ends_with($m[1], 'n".\'')) {
return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2);
}
return $m[1].$m[2];
}, $code, -1, $count);
if ($count && str_starts_with($code, "''.")) {
$code = substr($code, 3);
}
return $code;
}
if (\is_array($value)) {
$j = -1;
$code = '';
foreach ($value as $k => $v) {
$code .= $subIndent;
if (!\is_int($k) || 1 !== $k - $j) {
$code .= self::export($k, $subIndent).' => ';
}
if (\is_int($k) && $k > $j) {
$j = $k;
}
$code .= self::export($v, $subIndent).",\n";
}
return "[\n".$code.$indent.']';
}
if ($value instanceof Values) {
$code = $subIndent."\$r = [],\n";
foreach ($value->values as $k => $v) {
$code .= $subIndent.'$r['.$k.'] = '.self::export($v, $subIndent).",\n";
}
return "[\n".$code.$indent.']';
}
if ($value instanceof Registry) {
return self::exportRegistry($value, $indent, $subIndent);
}
if ($value instanceof Hydrator) {
return self::exportHydrator($value, $indent, $subIndent);
}
throw new \UnexpectedValueException(\sprintf('Cannot export value of type "%s".', get_debug_type($value)));
}
private static function exportRegistry(Registry $value, string $indent, string $subIndent): string
{
$code = '';
$serializables = [];
$seen = [];
$prototypesAccess = 0;
$factoriesAccess = 0;
$r = '\\'.Registry::class;
$j = -1;
foreach ($value->classes as $k => $class) {
if (':' === ($class[1] ?? null)) {
$serializables[$k] = $class;
continue;
}
if (!Registry::$instantiableWithoutConstructor[$class]) {
if (is_subclass_of($class, 'Serializable') && !method_exists($class, '__unserialize')) {
$serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}';
} else {
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}';
}
if (is_subclass_of($class, 'Throwable')) {
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
$serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4);
}
continue;
}
$code .= $subIndent.(1 !== $k - $j ? $k.' => ' : '');
$j = $k;
$eol = ",\n";
$c = '['.self::export($class).']';
if ($seen[$class] ?? false) {
if (Registry::$cloneable[$class]) {
++$prototypesAccess;
$code .= 'clone $p'.$c;
} else {
++$factoriesAccess;
$code .= '$f'.$c.'()';
}
} else {
$seen[$class] = true;
if (Registry::$cloneable[$class]) {
$code .= 'clone ('.($prototypesAccess++ ? '$p' : '($p = &'.$r.'::$prototypes)').$c.' ?? '.$r.'::p';
} else {
$code .= '('.($factoriesAccess++ ? '$f' : '($f = &'.$r.'::$factories)').$c.' ?? '.$r.'::f';
$eol = '()'.$eol;
}
$code .= '('.substr($c, 1, -1).'))';
}
$code .= $eol;
}
if (1 === $prototypesAccess) {
$code = str_replace('($p = &'.$r.'::$prototypes)', $r.'::$prototypes', $code);
}
if (1 === $factoriesAccess) {
$code = str_replace('($f = &'.$r.'::$factories)', $r.'::$factories', $code);
}
if ('' !== $code) {
$code = "\n".$code.$indent;
}
if ($serializables) {
$code = $r.'::unserialize(['.$code.'], '.self::export($serializables, $indent).')';
} else {
$code = '['.$code.']';
}
return '$o = '.$code;
}
private static function exportHydrator(Hydrator $value, string $indent, string $subIndent): string
{
$code = '';
foreach ($value->properties as $class => $properties) {
$code .= $subIndent.' '.self::export($class).' => '.self::export($properties, $subIndent.' ').",\n";
}
$code = [
self::export($value->registry, $subIndent),
self::export($value->values, $subIndent),
'' !== $code ? "[\n".$code.$subIndent.']' : '[]',
self::export($value->value, $subIndent),
self::export($value->wakeups, $subIndent),
];
return '\\'.$value::class."::hydrate(\n".$subIndent.implode(",\n".$subIndent, $code)."\n".$indent.')';
}
/**
* @param \ArrayIterator|\ArrayObject $value
* @param \ArrayIterator|\ArrayObject $proto
*/
private static function getArrayObjectProperties($value, $proto): array
{
$reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject';
$reflector = Registry::$reflectors[$reflector] ??= Registry::getClassReflector($reflector);
$properties = [
$arrayValue = (array) $value,
$reflector->getMethod('getFlags')->invoke($value),
$value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator',
];
$reflector = $reflector->getMethod('setFlags');
$reflector->invoke($proto, \ArrayObject::STD_PROP_LIST);
if ($properties[1] & \ArrayObject::STD_PROP_LIST) {
$reflector->invoke($value, 0);
$properties[0] = (array) $value;
} else {
$reflector->invoke($value, \ArrayObject::STD_PROP_LIST);
$arrayValue = (array) $value;
}
$reflector->invoke($value, $properties[1]);
if ([[], 0, 'ArrayIterator'] === $properties) {
$properties = [];
} else {
if ('ArrayIterator' === $properties[2]) {
unset($properties[2]);
}
$properties = [$reflector->class => ["\0" => $properties]];
}
return [$arrayValue, $properties];
}
}

View File

@@ -0,0 +1,320 @@
<?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\Exception\ClassNotFoundException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class Hydrator
{
public const PROPERTY_HAS_HOOKS = 1;
public const PROPERTY_NOT_BY_REF = 2;
public static array $hydrators = [];
public static array $simpleHydrators = [];
public static array $propertyScopes = [];
public function __construct(
public readonly Registry $registry,
public readonly ?Values $values,
public readonly array $properties,
public readonly mixed $value,
public readonly array $wakeups,
) {
}
public static function hydrate($objects, $values, $properties, $value, $wakeups)
{
foreach ($properties as $class => $vars) {
(self::$hydrators[$class] ??= self::getHydrator($class))($vars, $objects);
}
foreach ($wakeups as $k => $v) {
if (\is_array($v)) {
$objects[-$k]->__unserialize($v);
} else {
$objects[$v]->__wakeup();
}
}
return $value;
}
public static function getHydrator($class)
{
$baseHydrator = self::$hydrators['stdClass'] ??= static function ($properties, $objects) {
foreach ($properties as $name => $values) {
foreach ($values as $i => $v) {
$objects[$i]->$name = $v;
}
}
};
switch ($class) {
case 'stdClass':
return $baseHydrator;
case 'ErrorException':
return $baseHydrator->bindTo(null, new class extends \ErrorException {
});
case 'TypeError':
return $baseHydrator->bindTo(null, new class extends \Error {
});
case 'SplObjectStorage':
return static function ($properties, $objects) {
foreach ($properties as $name => $values) {
if ("\0" === $name) {
foreach ($values as $i => $v) {
for ($j = 0; $j < \count($v); ++$j) {
$objects[$i][$v[$j]] = $v[++$j];
}
}
continue;
}
foreach ($values as $i => $v) {
$objects[$i]->$name = $v;
}
}
};
}
if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new ClassNotFoundException($class);
}
$classReflector = new \ReflectionClass($class);
switch ($class) {
case 'ArrayIterator':
case 'ArrayObject':
$constructor = $classReflector->getConstructor()->invokeArgs(...);
return static function ($properties, $objects) use ($constructor) {
foreach ($properties as $name => $values) {
if ("\0" !== $name) {
foreach ($values as $i => $v) {
$objects[$i]->$name = $v;
}
}
}
foreach ($properties["\0"] ?? [] as $i => $v) {
$constructor($objects[$i], $v);
}
};
}
if (!$classReflector->isInternal()) {
return $baseHydrator->bindTo(null, $class);
}
if ($classReflector->name !== $class) {
return self::$hydrators[$classReflector->name] ??= self::getHydrator($classReflector->name);
}
$propertySetters = [];
foreach ($classReflector->getProperties() as $propertyReflector) {
if (!$propertyReflector->isStatic()) {
$propertySetters[$propertyReflector->name] = $propertyReflector->setValue(...);
}
}
if (!$propertySetters) {
return $baseHydrator;
}
return static function ($properties, $objects) use ($propertySetters) {
foreach ($properties as $name => $values) {
if ($setValue = $propertySetters[$name] ?? null) {
foreach ($values as $i => $v) {
$setValue($objects[$i], $v);
}
continue;
}
foreach ($values as $i => $v) {
$objects[$i]->$name = $v;
}
}
};
}
public static function getSimpleHydrator($class)
{
$baseHydrator = self::$simpleHydrators['stdClass'] ??= (function ($properties, $object) {
$notByRef = (array) $this;
foreach ($properties as $name => &$value) {
if (!$noRef = $notByRef[$name] ?? false) {
$object->$name = $value;
$object->$name = &$value;
} elseif (true !== $noRef) {
$noRef($object, $value);
} else {
$object->$name = $value;
}
}
})->bindTo(new \stdClass());
switch ($class) {
case 'stdClass':
return $baseHydrator;
case 'ErrorException':
return $baseHydrator->bindTo(new \stdClass(), new class extends \ErrorException {
});
case 'TypeError':
return $baseHydrator->bindTo(new \stdClass(), new class extends \Error {
});
case 'SplObjectStorage':
return static function ($properties, $object) {
foreach ($properties as $name => &$value) {
if ("\0" !== $name) {
$object->$name = $value;
$object->$name = &$value;
continue;
}
for ($i = 0; $i < \count($value); ++$i) {
$object[$value[$i]] = $value[++$i];
}
}
};
}
if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new ClassNotFoundException($class);
}
$classReflector = new \ReflectionClass($class);
switch ($class) {
case 'ArrayIterator':
case 'ArrayObject':
$constructor = $classReflector->getConstructor()->invokeArgs(...);
return static function ($properties, $object) use ($constructor) {
foreach ($properties as $name => &$value) {
if ("\0" === $name) {
$constructor($object, $value);
} else {
$object->$name = $value;
$object->$name = &$value;
}
}
};
}
if (!$classReflector->isInternal()) {
$notByRef = new \stdClass();
foreach ($classReflector->getProperties() as $propertyReflector) {
if ($propertyReflector->isStatic()) {
continue;
}
if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) {
$notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...);
} elseif ($propertyReflector->isReadOnly()) {
$notByRef->{$propertyReflector->name} = true;
}
}
return $baseHydrator->bindTo($notByRef, $class);
}
if ($classReflector->name !== $class) {
return self::$simpleHydrators[$classReflector->name] ??= self::getSimpleHydrator($classReflector->name);
}
$propertySetters = [];
foreach ($classReflector->getProperties() as $propertyReflector) {
if (!$propertyReflector->isStatic()) {
$propertySetters[$propertyReflector->name] = $propertyReflector->setValue(...);
}
}
if (!$propertySetters) {
return $baseHydrator;
}
return static function ($properties, $object) use ($propertySetters) {
foreach ($properties as $name => &$value) {
if ($setValue = $propertySetters[$name] ?? null) {
$setValue($object, $value);
} else {
$object->$name = $value;
$object->$name = &$value;
}
}
};
}
public static function getPropertyScopes($class): array
{
$propertyScopes = [];
$r = new \ReflectionClass($class);
foreach ($r->getProperties() as $property) {
$flags = $property->getModifiers();
if (\ReflectionProperty::IS_STATIC & $flags) {
continue;
}
$name = $property->name;
$access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0);
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);
}
if (\ReflectionProperty::IS_PRIVATE & $flags) {
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, null, $access, $property];
continue;
}
$propertyScopes[$name] = [$class, $name, null, $access, $property];
if ($flags & (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY)) {
$propertyScopes[$name][2] = $property->class;
}
if (\ReflectionProperty::IS_PROTECTED & $flags) {
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
}
}
while ($r = $r->getParentClass()) {
$class = $r->name;
foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) {
$flags = $property->getModifiers();
if (\ReflectionProperty::IS_STATIC & $flags) {
continue;
}
$name = $property->name;
$access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0);
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);
}
$propertyScopes["\0$class\0$name"] = [$class, $name, null, $access, $property];
$propertyScopes[$name] ??= $propertyScopes["\0$class\0$name"];
}
}
return $propertyScopes;
}
}

View File

@@ -0,0 +1,158 @@
<?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;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
/**
* @internal
*/
trait LazyDecoratorTrait
{
#[Ignore]
private readonly LazyObjectState $lazyObjectState;
/**
* Creates a lazy-loading decorator.
*
* @param \Closure():object $initializer Returns the proxied object
* @param static|null $instance
*/
public static function createLazyProxy(\Closure $initializer, ?object $instance = null): static
{
$class = $instance ? $instance::class : static::class;
if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
}
$instance ??= (Registry::$classReflectors[$class] ??= ($r = new \ReflectionClass($class))->hasProperty('lazyObjectState')
? $r
: throw new \LogicException('Cannot create a lazy proxy for a non-decorator object.')
)->newInstanceWithoutConstructor();
$state = $instance->lazyObjectState ??= new LazyObjectState();
$state->initializer = null;
unset($state->realInstance);
foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
$reset($instance, []);
}
$state->initializer = $initializer;
return $instance;
}
public function __construct(...$args)
{
self::createLazyProxy(static fn () => new parent(...$args), $this);
}
public function __destruct()
{
}
#[Ignore]
public function isLazyObjectInitialized(bool $partial = false): bool
{
return isset($this->lazyObjectState->realInstance);
}
public function initializeLazyObject(): parent
{
return $this->lazyObjectState->realInstance;
}
public function resetLazyObject(): bool
{
if (!isset($this->lazyObjectState->initializer)) {
return false;
}
unset($this->lazyObjectState->realInstance);
return true;
}
public function &__get($name): mixed
{
$instance = $this->lazyObjectState->realInstance;
$class = $this::class;
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
$notByRef = 0;
if ([, , , $access] = $propertyScopes[$name] ?? null) {
$notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF || ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET;
}
if ($notByRef || 2 !== ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['get'] ?: 2)) {
$value = $instance->$name;
return $value;
}
try {
return $instance->$name;
} catch (\Error $e) {
if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
throw $e;
}
try {
$instance->$name = [];
return $instance->$name;
} 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
{
$this->lazyObjectState->realInstance->$name = $value;
}
public function __isset($name): bool
{
return isset($this->lazyObjectState->realInstance->$name);
}
public function __unset($name): void
{
if ($this->lazyObjectState->initializer) {
unset($this->lazyObjectState->realInstance->$name);
}
}
public function __serialize(): array
{
return [$this->lazyObjectState->realInstance];
}
public function __unserialize($data): void
{
$this->lazyObjectState = new LazyObjectState();
$this->lazyObjectState->realInstance = $data[0];
}
public function __clone(): void
{
$this->lazyObjectState->realInstance; // initialize lazy object
$this->lazyObjectState = clone $this->lazyObjectState;
}
}

View File

@@ -0,0 +1,165 @@
<?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 objects and caches related reflection information.
*
* As a micro-optimization, this class uses no type declarations.
*
* @internal
*/
class LazyObjectRegistry
{
/**
* @var array<class-string, \ReflectionClass>
*/
public static array $classReflectors = [];
/**
* @var array<class-string, array<string, mixed>>
*/
public static array $defaultProperties = [];
/**
* @var array<class-string, list<\Closure>>
*/
public static array $classResetters = [];
/**
* @var array<class-string, array{get: \Closure, set: \Closure, isset: \Closure, unset: \Closure}>
*/
public static array $classAccessors = [];
/**
* @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 $parentMethods = [];
public static ?\Closure $noInitializerState = null;
public static function getClassResetters($class)
{
$classProperties = [];
$hookedProperties = [];
if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) {
$propertyScopes = [];
} else {
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
}
foreach ($propertyScopes as $key => [$scope, $name, $writeScope, $access]) {
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
if ($k !== $key || "\0$class\0lazyObjectState" === $k) {
continue;
}
if ($access & Hydrator::PROPERTY_HAS_HOOKS) {
$hookedProperties[$k] = true;
} else {
$classProperties[$writeScope ?? $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);
}
return $resetters;
}
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 = [];
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();
}
}
$methods['get'] = $methods['get'] ? ($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

@@ -0,0 +1,120 @@
<?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 as PublicHydrator;
/**
* Keeps the state of lazy objects.
*
* As a micro-optimization, this class uses no type declarations.
*
* @internal
*/
class LazyObjectState
{
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)) {
try {
$this->realInstance = $this->cloneInstance;
} finally {
unset($this->cloneInstance);
}
} elseif (isset($this->realInstance)) {
$this->realInstance = clone $this->realInstance;
}
}
public function __get($name)
{
if ('realInstance' !== $name) {
throw new \BadMethodCallException(\sprintf('No such property "%s::$%s"', self::class, $name));
}
return $this->realInstance = ($this->initializer)();
}
}

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,28 @@
<?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;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class Reference
{
public int $count = 0;
public function __construct(
public readonly int $id,
public readonly mixed $value = null,
) {
}
}

View File

@@ -0,0 +1,142 @@
<?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\Exception\ClassNotFoundException;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class Registry
{
public static array $reflectors = [];
public static array $prototypes = [];
public static array $factories = [];
public static array $cloneable = [];
public static array $instantiableWithoutConstructor = [];
public function __construct(
public readonly array $classes,
) {
}
public static function unserialize($objects, $serializables)
{
$unserializeCallback = ini_set('unserialize_callback_func', __CLASS__.'::getClassReflector');
try {
foreach ($serializables as $k => $v) {
$objects[$k] = unserialize($v);
}
} finally {
ini_set('unserialize_callback_func', $unserializeCallback);
}
return $objects;
}
public static function p($class)
{
self::getClassReflector($class, true, true);
return self::$prototypes[$class];
}
public static function f($class)
{
$reflector = self::$reflectors[$class] ??= self::getClassReflector($class, true, false);
return self::$factories[$class] = $reflector->newInstanceWithoutConstructor(...);
}
public static function getClassReflector($class, $instantiableWithoutConstructor = false, $cloneable = null)
{
if (!($isClass = class_exists($class)) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new ClassNotFoundException($class);
}
$reflector = new \ReflectionClass($class);
if ($instantiableWithoutConstructor) {
$proto = $reflector->newInstanceWithoutConstructor();
} elseif (!$isClass || $reflector->isAbstract()) {
throw new NotInstantiableTypeException($class);
} elseif ($reflector->name !== $class) {
$reflector = self::$reflectors[$name = $reflector->name] ??= self::getClassReflector($name, false, $cloneable);
self::$cloneable[$class] = self::$cloneable[$name];
self::$instantiableWithoutConstructor[$class] = self::$instantiableWithoutConstructor[$name];
self::$prototypes[$class] = self::$prototypes[$name];
return $reflector;
} else {
try {
$proto = $reflector->newInstanceWithoutConstructor();
$instantiableWithoutConstructor = true;
} catch (\ReflectionException) {
$proto = $reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize') ? 'C:' : 'O:';
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
$proto = null;
} else {
try {
$proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}');
} catch (\Exception $e) {
if (__FILE__ !== $e->getFile()) {
throw $e;
}
throw new NotInstantiableTypeException($class, $e);
}
if (false === $proto) {
throw new NotInstantiableTypeException($class);
}
}
}
if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !method_exists($class, '__sleep') && !method_exists($class, '__serialize')) {
try {
serialize($proto);
} catch (\Exception $e) {
throw new NotInstantiableTypeException($class, $e);
}
}
}
if (null === $cloneable) {
if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !method_exists($proto, '__wakeup') && !method_exists($class, '__unserialize'))) {
throw new NotInstantiableTypeException($class);
}
$cloneable = $reflector->isCloneable() && !$reflector->hasMethod('__clone');
}
self::$cloneable[$class] = $cloneable;
self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor;
self::$prototypes[$class] = $proto;
if ($proto instanceof \Throwable) {
static $setTrace;
if (null === $setTrace) {
$setTrace = [
new \ReflectionProperty(\Error::class, 'trace'),
new \ReflectionProperty(\Exception::class, 'trace'),
];
$setTrace[0] = $setTrace[0]->setValue(...);
$setTrace[1] = $setTrace[1]->setValue(...);
}
$setTrace[$proto instanceof \Exception]($proto, []);
}
return $reflector;
}
}

View File

@@ -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\Internal;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class Values
{
public function __construct(
public readonly array $values,
) {
}
}

19
vendor/symfony/var-exporter/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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,32 @@
<?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 LazyObjectInterface
{
/**
* Returns whether the object is initialized.
*
* @param bool $partial Whether partially initialized objects should be considered as initialized
*/
public function isLazyObjectInitialized(bool $partial = false): bool;
/**
* Forces initialization of a lazy object and returns it.
*/
public function initializeLazyObject(): object;
/**
* @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
*/
public function resetLazyObject(): bool;
}

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

@@ -0,0 +1,708 @@
<?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\VarExporter\Exception\LogicException;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\LazyDecoratorTrait;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
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.
*
* @param \ReflectionClass[] $interfaces
*
* @throws LogicException When the class is incompatible with virtual proxies
*/
public static function generateLazyProxy(?\ReflectionClass $class, array $interfaces = []): string
{
if (!class_exists($class?->name ?? \stdClass::class, false)) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: "%s" is not a class.', $class->name));
}
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;
do {
$extendsInternalClass = $parent->isInternal();
} while (!$extendsInternalClass && $parent = $parent->getParentClass());
if (!$extendsInternalClass) {
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));
}
}
$propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : [];
$abstractProperties = [];
$hookedProperties = [];
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 || $flags & \ReflectionProperty::IS_PRIVATE) {
continue;
}
if ($flags & \ReflectionProperty::IS_ABSTRACT) {
$abstractProperties[$name] = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
continue;
}
$abstractProperties[$name] = false;
if (!($access & Hydrator::PROPERTY_HAS_HOOKS)) {
continue;
}
if ($flags & \ReflectionProperty::IS_FINAL) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: property "%s::$%s" is final.', $class->name, $name));
}
$p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
$hookedProperties[$name] = [$p, $p->getHooks()];
}
$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();
foreach ($interface->getProperties() as $p) {
$abstractProperties[$p->name] ??= $p;
$hookedProperties[$p->name] ??= [$p, []];
$hookedProperties[$p->name][1] += $p->getHooks();
}
}
$hooks = '';
foreach (array_filter($abstractProperties) as $name => $p) {
$type = self::exportType($p);
$hooks .= "\n "
.($p->isProtected() ? 'protected' : 'public')
.($p->isProtectedSet() ? ' protected(set)' : '')
." {$type} \${$name};\n";
}
foreach ($hookedProperties as $name => [$p, $methods]) {
if ($abstractProperties[$p->name] ?? false) {
continue;
}
$type = self::exportType($p);
$hooks .= "\n "
.($p->isProtected() ? 'protected' : 'public')
.($p->isProtectedSet() ? ' protected(set)' : '')
." {$type} \${$name} {\n";
foreach ($methods as $hook => $method) {
if ('get' === $hook) {
$ref = ($method->returnsReference() ? '&' : '');
$hooks .= <<<EOPHP
{$ref}get {
return \$this->lazyObjectState->realInstance->{$p->name};
}
EOPHP;
} elseif ('set' === $hook) {
$parameters = self::exportParameters($method, true);
$arg = '$'.$method->getParameters()[0]->name;
$hooks .= <<<EOPHP
set({$parameters}) {
\$this->lazyObjectState->realInstance->{$p->name} = {$arg};
}
EOPHP;
} else {
throw new LogicException(\sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name));
}
}
$hooks .= " }\n";
}
$methods = [];
$methodReflectors = array_merge(...$methodReflectors);
foreach ($methodReflectors as $method) {
if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
continue;
}
$trait = new \ReflectionMethod(LazyDecoratorTrait::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()) {
throw new LogicException(\sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.', $class->name, $method->name));
}
if (method_exists(LazyDecoratorTrait::class, $method->name)) {
continue;
}
$signature = self::exportSignature($method, true, $args);
if ($method->isStatic()) {
$body = " throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".');";
} elseif (str_ends_with($signature, '): never') || str_ends_with($signature, '): void')) {
$body = <<<EOPHP
\$this->lazyObjectState->realInstance->{$method->name}({$args});
EOPHP;
} else {
$mayReturnThis = false;
foreach (preg_split('/[()|&]++/', self::exportType($method) ?? 'static') as $type) {
if (\in_array($type = ltrim($type, '?'), ['static', 'object'], true)) {
$mayReturnThis = true;
break;
}
foreach ([$class, ...$interfaces] as $r) {
if ($r && is_a($r->name, $type, true)) {
$mayReturnThis = true;
break 2;
}
}
}
if ($method->returnsReference() || !$mayReturnThis) {
$body = <<<EOPHP
return \$this->lazyObjectState->realInstance->{$method->name}({$args});
EOPHP;
} else {
$body = <<<EOPHP
\${0} = \$this->lazyObjectState->realInstance;
\${1} = \${0}->{$method->name}({$args});
return match (true) {
\${1} === \${0} => \$this,
!\${1} instanceof \${0} || !\${0} instanceof \${1} => \${1},
null !== \$this->lazyObjectState->cloneInstance =& \${1} => clone \$this,
};
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(LazyDecoratorTrait::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) : '[]';
$lazyProxyTraitStatement = [];
if (
$class?->hasMethod('__unserialize')
&& !$class->getMethod('__unserialize')->getParameters()[0]->getType()
) {
// fix contravariance type problem when $class declares a `__unserialize()` method without typehint.
$lazyProxyTraitStatement[] = '__unserialize as private __doUnserialize;';
$body .= <<<EOPHP
public function __unserialize(\$data): void
{
\$this->__doUnserialize(\$data);
}
EOPHP;
}
if ($lazyProxyTraitStatement) {
$lazyProxyTraitStatement = implode("\n ", $lazyProxyTraitStatement);
$lazyProxyTraitStatement = <<<EOPHP
use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait {
{$lazyProxyTraitStatement}
}
EOPHP;
} else {
$lazyProxyTraitStatement = <<<EOPHP
use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait;
EOPHP;
}
return <<<EOPHP
{$parent} implements \\{$interfaces}
{
{$lazyProxyTraitStatement}
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
{$hooks}{$body}}
// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
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;
$args = '';
$param = null;
$parameters = [];
$namespace = $function instanceof \ReflectionMethod ? $function->class : $function->getNamespaceName().'\\';
$namespace = substr($namespace, 0, strrpos($namespace, '\\') ?: 0);
foreach ($function->getParameters() as $param) {
$parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' : '')
.($withParameterTypes && $param->hasType() ? self::exportType($param).' ' : '')
.($param->isPassedByReference() ? '&' : '')
.($param->isVariadic() ? '...' : '').'$'.$param->name
.($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param, $namespace) : '');
if ($param->isPassedByReference()) {
$byRefIndex = 1 + $param->getPosition();
}
$args .= ($param->isVariadic() ? '...$' : '$').$param->name.', ';
}
if (!$param || !$byRefIndex) {
$args = '...\func_get_args()';
} elseif ($param->isVariadic()) {
$args = substr($args, 0, -2);
} else {
$args = explode(', ', $args, 1 + $byRefIndex);
$args[$byRefIndex] = \sprintf('...\array_slice(\func_get_args(), %d)', $byRefIndex);
$args = implode(', ', $args);
}
return implode(', ', $parameters);
}
public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
{
$parameters = self::exportParameters($function, $withParameterTypes, $args);
$signature = 'function '.($function->returnsReference() ? '&' : '')
.($function->isClosure() ? '' : $function->name).'('.$parameters.')';
if ($function instanceof \ReflectionMethod) {
$signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private '))
.($function->isStatic() ? 'static ' : '').$signature;
}
if ($function->hasReturnType()) {
$signature .= ': '.self::exportType($function);
}
static $getPrototype;
$getPrototype ??= (new \ReflectionMethod(\ReflectionMethod::class, 'getPrototype'))->invoke(...);
while ($function) {
if ($function->hasTentativeReturnType()) {
return '#[\ReturnTypeWillChange] '.$signature;
}
try {
$function = $function instanceof \ReflectionMethod && $function->isAbstract() ? false : $getPrototype($function);
} catch (\ReflectionException) {
break;
}
}
return $signature;
}
public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $owner, bool $noBuiltin = false, ?\ReflectionType $type = null): ?string
{
if (!$type ??= $owner instanceof \ReflectionFunctionAbstract ? $owner->getReturnType() : $owner->getType()) {
return null;
}
$class = null;
$types = [];
if ($type instanceof \ReflectionUnionType) {
$reflectionTypes = $type->getTypes();
$glue = '|';
} elseif ($type instanceof \ReflectionIntersectionType) {
$reflectionTypes = $type->getTypes();
$glue = '&';
} else {
$reflectionTypes = [$type];
$glue = null;
}
foreach ($reflectionTypes as $type) {
if ($type instanceof \ReflectionIntersectionType) {
if ('' !== $name = '('.self::exportType($owner, $noBuiltin, $type).')') {
$types[] = $name;
}
continue;
}
$name = $type->getName();
if ($noBuiltin && $type->isBuiltin()) {
continue;
}
if (\in_array($name, ['parent', 'self'], true) && $class ??= $owner->getDeclaringClass()) {
$name = 'parent' === $name ? ($class->getParentClass() ?: null)?->name ?? 'parent' : $class->name;
}
$types[] = ($noBuiltin || $type->isBuiltin() || 'static' === $name ? '' : '\\').$name;
}
if (!$types) {
return '';
}
if (null === $glue) {
$defaultNull = $owner instanceof \ReflectionParameter && 'NULL' === rtrim(substr(explode('$'.$owner->name.' = ', (string) $owner, 2)[1] ?? '', 0, -2));
return (!$noBuiltin && ($type->allowsNull() || $defaultNull) && !\in_array($name, ['mixed', 'null'], true) ? '?' : '').$types[0];
}
sort($types);
return implode($glue, $types);
}
private static function exportPropertyScopes(string $parent, array $propertyScopes): string
{
uksort($propertyScopes, 'strnatcmp');
foreach ($propertyScopes as $k => $v) {
unset($propertyScopes[$k][4]);
}
$propertyScopes = VarExporter::export($propertyScopes);
$propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes);
$propertyScopes = preg_replace("/(?|(,)\n( ) |\n |,\n (\]))/", '$1$2', $propertyScopes);
return str_replace("\n", "\n ", $propertyScopes);
}
private static function exportDefault(\ReflectionParameter $param, $namespace): string
{
$default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2));
if (\in_array($default, ['<default>', 'NULL'], true)) {
return 'null';
}
if (str_ends_with($default, "...'") && preg_match("/^'(?:[^'\\\\]*+(?:\\\\.)*+)*+'$/", $default)) {
return VarExporter::export($param->getDefaultValue());
}
$regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/";
$parts = preg_split($regexp, $default, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
$regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(\(?)(?!: )/';
$callback = (false !== strpbrk($default, "\\:('") && $class = $param->getDeclaringClass())
? fn ($m) => $m[1].match ($m[2]) {
'new', 'false', 'true', 'null' => $m[2],
'NULL' => 'null',
'self' => '\\'.$class->name,
'namespace\\parent',
'parent' => ($parent = $class->getParentClass()) ? '\\'.$parent->name : 'parent',
default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
}.$m[3]
: fn ($m) => $m[1].match ($m[2]) {
'new', 'false', 'true', 'null', 'self', 'parent' => $m[2],
'NULL' => 'null',
default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
}.$m[3];
return implode('', array_map(fn ($part) => match ($part[0]) {
'"' => $part, // for internal classes only
"'" => false !== strpbrk($part, "\\\0\r\n") ? '"'.substr(str_replace(['$', "\0", "\r", "\n"], ['\$', '\0', '\r', '\n'], $part), 1, -1).'"' : $part,
default => preg_replace_callback($regexp, $callback, $part),
}, $parts));
}
private static function exportSymbol(string $symbol, bool $mightBeRootConst, string $namespace): string
{
if (!$mightBeRootConst
|| false === ($ns = strrpos($symbol, '\\'))
|| substr($symbol, 0, $ns) !== $namespace
|| \defined($symbol)
|| !\defined(substr($symbol, $ns + 1))
) {
return '\\'.$symbol;
}
return '\\'.substr($symbol, $ns + 1);
}
}

98
vendor/symfony/var-exporter/README.md vendored Normal file
View File

@@ -0,0 +1,98 @@
VarExporter Component
=====================
The VarExporter component provides various tools to deal with the internal state
of objects:
- `VarExporter::export()` allows exporting any serializable PHP data structure to
plain PHP code. While doing so, it preserves all the semantics associated with
the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`,
`__serialize`, `__unserialize`);
- `Instantiator::instantiate()` creates an object and sets its properties without
calling its constructor nor any other methods;
- `Hydrator::hydrate()` can set the properties of an existing object;
- `Lazy*Trait` can make a class behave as a lazy-loading ghost or virtual proxy.
VarExporter::export()
---------------------
The reason to use `VarExporter::export()` *vs* `serialize()` or
[igbinary](https://github.com/igbinary/igbinary) is performance: thanks to
OPcache, the resulting code is significantly faster and more memory efficient
than using `unserialize()` or `igbinary_unserialize()`.
Unlike `var_export()`, this works on any serializable PHP value.
It also provides a few improvements over `var_export()`/`serialize()`:
* the output is PSR-2 compatible;
* the output can be re-indented without messing up with `\r` or `\n` in the data;
* missing classes throw a `ClassNotFoundException` instead of being unserialized
to `PHP_Incomplete_Class` objects;
* references involving `SplObjectStorage`, `ArrayObject` or `ArrayIterator`
instances are preserved;
* `Reflection*`, `IteratorIterator` and `RecursiveIteratorIterator` classes
throw an exception when being serialized (their unserialized version is broken
anyway, see https://bugs.php.net/76737).
Instantiator and Hydrator
-------------------------
`Instantiator::instantiate($class)` creates an object of the given class without
calling its constructor nor any other methods.
`Hydrator::hydrate()` sets the properties of an existing object, including
private and protected ones. For example:
```php
// Sets the public or protected $object->propertyName property
Hydrator::hydrate($object, ['propertyName' => $propertyValue]);
// Sets a private property defined on its parent Bar class:
Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]);
// Alternative way to set the private $object->privateBarProperty property
Hydrator::hydrate($object, [], [
Bar::class => ['privateBarProperty' => $propertyValue],
]);
```
Lazy Proxies
------------
Since version 8.4, PHP provides support for lazy objects via the reflection API.
This native API works with concrete classes. It doesn't with abstracts nor with
internal ones.
This components provides helpers to generate lazy objects using the decorator
pattern, which works with abstract or internal classes and with interfaces:
```php
$proxyCode = ProxyHelper::generateLazyProxy(new ReflectionClass(AbstractFoo::class));
// $proxyCode should be dumped into a file in production envs
eval('class FooLazyProxy'.$proxyCode);
$foo = FooLazyProxy::createLazyProxy(initializer: function (): AbstractFoo {
// [...] Use whatever heavy logic you need here
// to compute the $dependencies of the $instance
$instance = new Foo(...$dependencies);
// [...] Call setters, etc. if needed
return $instance;
});
// $foo is now a lazy-loading decorator object. The initializer will
// 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
---------
* [Documentation](https://symfony.com/doc/current/components/var_exporter.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,114 @@
<?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\VarExporter\Exception\ExceptionInterface;
use Symfony\Component\VarExporter\Internal\Exporter;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\Registry;
use Symfony\Component\VarExporter\Internal\Values;
/**
* Exports serializable PHP values to PHP code.
*
* VarExporter allows serializing PHP data structures to plain PHP code (like var_export())
* while preserving all the semantics associated with serialize() (unlike var_export()).
*
* By leveraging OPcache, the generated PHP code is faster than doing the same with unserialize().
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class VarExporter
{
/**
* Exports a serializable PHP value to PHP code.
*
* @param bool &$isStaticValue Set to true after execution if the provided value is static, false otherwise
* @param array<class-string, class-string> &$foundClasses Classes found in the value are added to this list as both keys and values
*
* @throws ExceptionInterface When the provided value cannot be serialized
*/
public static function export(mixed $value, ?bool &$isStaticValue = null, array &$foundClasses = []): string
{
$isStaticValue = true;
if (!\is_object($value) && !(\is_array($value) && $value) && !\is_resource($value) || $value instanceof \UnitEnum) {
return Exporter::export($value);
}
$objectsPool = new \SplObjectStorage();
$refsPool = [];
$objectsCount = 0;
try {
$value = Exporter::prepare([$value], $objectsPool, $refsPool, $objectsCount, $isStaticValue)[0];
} finally {
$references = [];
foreach ($refsPool as $i => $v) {
if ($v[0]->count) {
$references[1 + $i] = $v[2];
}
$v[0] = $v[1];
}
}
if ($isStaticValue) {
return Exporter::export($value);
}
$classes = [];
$values = [];
$states = [];
foreach ($objectsPool as $i => $v) {
[, $class, $values[], $wakeup] = $objectsPool[$v];
$foundClasses[$class] = $classes[] = $class;
if (0 < $wakeup) {
$states[$wakeup] = $i;
} elseif (0 > $wakeup) {
$states[-$wakeup] = [$i, array_pop($values)];
$values[] = [];
}
}
ksort($states);
$wakeups = [null];
foreach ($states as $v) {
if (\is_array($v)) {
$wakeups[-$v[0]] = $v[1];
} else {
$wakeups[] = $v;
}
}
if (null === $wakeups[0]) {
unset($wakeups[0]);
}
$properties = [];
foreach ($values as $i => $vars) {
foreach ($vars as $class => $values) {
foreach ($values as $name => $v) {
$properties[$class][$name][$i] = $v;
}
}
}
if ($classes || $references) {
$value = new Hydrator(new Registry($classes), $references ? new Values($references) : null, $properties, $value, $wakeups);
} else {
$isStaticValue = true;
}
return Exporter::export($value);
}
}

View File

@@ -0,0 +1,34 @@
{
"name": "symfony/var-exporter",
"type": "library",
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
"keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone", "lazy-loading", "proxy"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/property-access": "^6.4|^7.0",
"symfony/serializer": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\VarExporter\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}