From e1a8502376dd81e94a6c456a710b353a48f414a7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 24 May 2022 19:31:57 +0200 Subject: [PATCH] [VarExporter] Add `Hydrator::hydrate()` and preserve PHP references when using it --- .../Component/VarExporter/CHANGELOG.md | 7 + .../Component/VarExporter/Hydrator.php | 78 +++++++++ .../Component/VarExporter/Instantiator.php | 61 ++----- .../VarExporter/Internal/Exporter.php | 6 +- .../VarExporter/Internal/Hydrator.php | 159 ++++++++++++++++-- .../VarExporter/Internal/Registry.php | 8 +- .../VarExporter/Tests/InstantiatorTest.php | 28 ++- 7 files changed, 275 insertions(+), 72 deletions(-) create mode 100644 src/Symfony/Component/VarExporter/Hydrator.php diff --git a/src/Symfony/Component/VarExporter/CHANGELOG.md b/src/Symfony/Component/VarExporter/CHANGELOG.md index 3406c30efb4bf..011ac96058cc9 100644 --- a/src/Symfony/Component/VarExporter/CHANGELOG.md +++ b/src/Symfony/Component/VarExporter/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +6.2 +--- + + * 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 ----- diff --git a/src/Symfony/Component/VarExporter/Hydrator.php b/src/Symfony/Component/VarExporter/Hydrator.php new file mode 100644 index 0000000000000..0827481778b1e --- /dev/null +++ b/src/Symfony/Component/VarExporter/Hydrator.php @@ -0,0 +1,78 @@ + + * + * 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 + */ +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 $properties The properties to set on the instance + * @param array> $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 = \get_class($instance); + $propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class); + + foreach ($properties as $name => &$value) { + [$scope, $name] = $propertyScopes[$name] ?? [$class, $name]; + $scopedProperties[$scope][$name] = &$value; + } + unset($value); + } + + foreach ($scopedProperties as $class => $properties) { + if ($properties) { + (InternalHydrator::$simpleHydrators[$class] ??= InternalHydrator::getSimpleHydrator($class))($properties, $instance); + } + } + + return $instance; + } +} diff --git a/src/Symfony/Component/VarExporter/Instantiator.php b/src/Symfony/Component/VarExporter/Instantiator.php index 4a9c1c6deac73..9a35b161bbd4f 100644 --- a/src/Symfony/Component/VarExporter/Instantiator.php +++ b/src/Symfony/Component/VarExporter/Instantiator.php @@ -13,7 +13,6 @@ use Symfony\Component\VarExporter\Exception\ExceptionInterface; use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; -use Symfony\Component\VarExporter\Internal\Hydrator; use Symfony\Component\VarExporter\Internal\Registry; /** @@ -26,67 +25,35 @@ final class Instantiator /** * Creates an object and sets its properties without calling its constructor nor any other methods. * - * For example: + * @see Hydrator::hydrate() for examples * - * // creates an empty instance of Foo - * Instantiator::instantiate(Foo::class); + * @template T of object * - * // creates a Foo instance and sets one of its properties - * Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]); + * @param class-string $class The class of the instance to create + * @param array $properties The properties to set on the instance + * @param array> $scopedProperties The properties to set on the instance, + * keyed by their declaring class * - * // creates a Foo instance and sets a private property defined on its parent Bar class - * Instantiator::instantiate(Foo::class, [], [ - * Bar::class => ['privateBarProperty' => $propertyValue], - * ]); - * - * Instances of ArrayObject, ArrayIterator and SplObjectHash can be created - * by using the special "\0" property name to define their internal value: - * - * // creates an SplObjectHash where $info1 is attached to $obj1, etc. - * Instantiator::instantiate(SplObjectStorage::class, ["\0" => [$obj1, $info1, $obj2, $info2...]]); - * - * // creates an ArrayObject populated with $inputArray - * Instantiator::instantiate(ArrayObject::class, ["\0" => [$inputArray]]); - * - * @param string $class The class of the instance to create - * @param array $properties The properties to set on the instance - * @param array $privateProperties The private 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 $privateProperties = []): object + public static function instantiate(string $class, array $properties = [], array $scopedProperties = []): object { - $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + $reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class); if (Registry::$cloneable[$class]) { - $wrappedInstance = [clone Registry::$prototypes[$class]]; + $instance = clone Registry::$prototypes[$class]; } elseif (Registry::$instantiableWithoutConstructor[$class]) { - $wrappedInstance = [$reflector->newInstanceWithoutConstructor()]; + $instance = $reflector->newInstanceWithoutConstructor(); } elseif (null === Registry::$prototypes[$class]) { throw new NotInstantiableTypeException($class); } elseif ($reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize')) { - $wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')]; + $instance = unserialize('C:'.\strlen($class).':"'.$class.'":0:{}'); } else { - $wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')]; - } - - if ($properties) { - $privateProperties[$class] = isset($privateProperties[$class]) ? $properties + $privateProperties[$class] : $properties; - } - - foreach ($privateProperties as $class => $properties) { - if (!$properties) { - continue; - } - foreach ($properties as $name => $value) { - // because they're also used for "unserialization", hydrators - // deal with array of instances, so we need to wrap values - $properties[$name] = [$value]; - } - (Hydrator::$hydrators[$class] ?? Hydrator::getHydrator($class))($properties, $wrappedInstance); + $instance = unserialize('O:'.\strlen($class).':"'.$class.'":0:{}'); } - return $wrappedInstance[0]; + return Hydrator::hydrate($instance, $properties, $scopedProperties); } } diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index 0a620ababfeae..618f2a0c72760 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -72,7 +72,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount } $class = \get_class($value); - $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + $reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class); if ($reflector->hasMethod('__serialize')) { if (!$reflector->getMethod('__serialize')->isPublic()) { @@ -108,7 +108,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $arrayValue = (array) $value; } elseif ($value instanceof \Serializable || $value instanceof \__PHP_Incomplete_Class - || PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod + || \PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod ) { ++$objectsCount; $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0]; @@ -372,7 +372,7 @@ private static function exportHydrator(Hydrator $value, string $indent, string $ private static function getArrayObjectProperties($value, $proto): array { $reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject'; - $reflector = Registry::$reflectors[$reflector] ?? Registry::getClassReflector($reflector); + $reflector = Registry::$reflectors[$reflector] ??= Registry::getClassReflector($reflector); $properties = [ $arrayValue = (array) $value, diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 9e73f2b16840e..221d04f234664 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -21,6 +21,8 @@ class Hydrator { public static $hydrators = []; + public static $simpleHydrators = []; + public static $propertyScopes = []; public $registry; public $values; @@ -40,7 +42,7 @@ public function __construct(?Registry $registry, ?Values $values, array $propert public static function hydrate($objects, $values, $properties, $value, $wakeups) { foreach ($properties as $class => $vars) { - (self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects); + (self::$hydrators[$class] ??= self::getHydrator($class))($vars, $objects); } foreach ($wakeups as $k => $v) { if (\is_array($v)) { @@ -55,26 +57,28 @@ public static function hydrate($objects, $values, $properties, $value, $wakeups) 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 self::$hydrators[$class] = static function ($properties, $objects) { - foreach ($properties as $name => $values) { - foreach ($values as $i => $v) { - $objects[$i]->$name = $v; - } - } - }; + return $baseHydrator; case 'ErrorException': - return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \ErrorException { + return $baseHydrator->bindTo(null, new class() extends \ErrorException { }); case 'TypeError': - return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \Error { + return $baseHydrator->bindTo(null, new class() extends \Error { }); case 'SplObjectStorage': - return self::$hydrators[$class] = static function ($properties, $objects) { + return static function ($properties, $objects) { foreach ($properties as $name => $values) { if ("\0" === $name) { foreach ($values as $i => $v) { @@ -101,7 +105,7 @@ public static function getHydrator($class) case 'ArrayObject': $constructor = $classReflector->getConstructor()->invokeArgs(...); - return self::$hydrators[$class] = static function ($properties, $objects) use ($constructor) { + return static function ($properties, $objects) use ($constructor) { foreach ($properties as $name => $values) { if ("\0" !== $name) { foreach ($values as $i => $v) { @@ -116,11 +120,11 @@ public static function getHydrator($class) } if (!$classReflector->isInternal()) { - return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, $class); + return $baseHydrator->bindTo(null, $class); } if ($classReflector->name !== $class) { - return self::$hydrators[$classReflector->name] ?? self::getHydrator($classReflector->name); + return self::$hydrators[$classReflector->name] ??= self::getHydrator($classReflector->name); } $propertySetters = []; @@ -131,10 +135,10 @@ public static function getHydrator($class) } if (!$propertySetters) { - return self::$hydrators[$class] = self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'); + return $baseHydrator; } - return self::$hydrators[$class] = static function ($properties, $objects) use ($propertySetters) { + return static function ($properties, $objects) use ($propertySetters) { foreach ($properties as $name => $values) { if ($setValue = $propertySetters[$name] ?? null) { foreach ($values as $i => $v) { @@ -148,4 +152,127 @@ public static function getHydrator($class) } }; } + + public static function getSimpleHydrator($class) + { + $baseHydrator = self::$simpleHydrators['stdClass'] ??= static function ($properties, $object) { + foreach ($properties as $name => &$value) { + $object->$name = &$value; + } + }; + + 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, $object) { + foreach ($properties as $name => &$value) { + if ("\0" !== $name) { + $object->$name = &$value; + continue; + } + for ($i = 0; $i < \count($value); ++$i) { + $object->attach($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; + } + } + }; + } + + if (!$classReflector->isInternal()) { + return $baseHydrator->bindTo(null, $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; + } + } + }; + } + + public static function getPropertyScopes($class) + { + $propertyScopes = []; + $r = new \ReflectionClass($class); + + foreach ($r->getProperties() as $property) { + $flags = $property->getModifiers(); + + if (\ReflectionProperty::IS_STATIC & $flags) { + continue; + } + $name = $property->name; + + if (\ReflectionProperty::IS_PRIVATE & $flags) { + $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name]; + continue; + } + $propertyScopes[$name] = [$flags & \ReflectionProperty::IS_READONLY ? $property->class : $class, $name]; + + 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) { + if (!$property->isStatic()) { + $name = $property->name; + $propertyScopes["\0$class\0$name"] = [$class, $name]; + } + } + } + + return $propertyScopes; + } } diff --git a/src/Symfony/Component/VarExporter/Internal/Registry.php b/src/Symfony/Component/VarExporter/Internal/Registry.php index 119145eeb9eb7..09d2de2a05ab9 100644 --- a/src/Symfony/Component/VarExporter/Internal/Registry.php +++ b/src/Symfony/Component/VarExporter/Internal/Registry.php @@ -58,7 +58,7 @@ public static function p($class) public static function f($class) { - $reflector = self::$reflectors[$class] ?? self::getClassReflector($class, true, false); + $reflector = self::$reflectors[$class] ??= self::getClassReflector($class, true, false); return self::$factories[$class] = [$reflector, 'newInstanceWithoutConstructor'](...); } @@ -75,12 +75,12 @@ public static function getClassReflector($class, $instantiableWithoutConstructor } elseif (!$isClass || $reflector->isAbstract()) { throw new NotInstantiableTypeException($class); } elseif ($reflector->name !== $class) { - $reflector = self::$reflectors[$name = $reflector->name] ?? self::getClassReflector($name, false, $cloneable); + $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 self::$reflectors[$class] = $reflector; + return $reflector; } else { try { $proto = $reflector->newInstanceWithoutConstructor(); @@ -139,6 +139,6 @@ public static function getClassReflector($class, $instantiableWithoutConstructor $setTrace[$proto instanceof \Exception]($proto, []); } - return self::$reflectors[$class] = $reflector; + return $reflector; } } diff --git a/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php b/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php index cbd223642320b..becf4208c93f5 100644 --- a/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php +++ b/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php @@ -51,12 +51,19 @@ public function testInstantiate() $this->assertEquals(new \ArrayObject([123]), Instantiator::instantiate(\ArrayObject::class, ["\0" => [[123]]])); $expected = [ + "\0*\0prot" => 345, "\0".__NAMESPACE__."\Bar\0priv" => 123, "\0".__NAMESPACE__."\Foo\0priv" => 234, - 'dyn' => 345, + 'dyn' => 456, + 'ro' => 567, ]; - $actual = (array) Instantiator::instantiate(Bar::class, ['dyn' => 345, 'priv' => 123], [Foo::class => ['priv' => 234]]); + $actual = (array) Instantiator::instantiate(Bar::class, ['dyn' => 456, 'ro' => 567, 'prot' => 345, 'priv' => 123], [Foo::class => ['priv' => 234]]); + ksort($actual); + + $this->assertSame($expected, $actual); + + $actual = (array) Instantiator::instantiate(Bar::class, $expected); ksort($actual); $this->assertSame($expected, $actual); @@ -65,11 +72,28 @@ public function testInstantiate() $this->assertSame([234], $e->getTrace()); } + + public function testPhpReferences() + { + $properties = ['p1' => 1]; + $properties['p2'] = &$properties['p1']; + + $obj = Instantiator::instantiate('stdClass', $properties); + + $this->assertSame($properties, (array) $obj); + + $properties['p1'] = 2; + $this->assertSame(2, $properties['p2']); + $this->assertSame(2, $obj->p1); + $this->assertSame(2, $obj->p2); + } } class Foo { + protected $prot; private $priv; + public readonly int $ro; } #[\AllowDynamicProperties]