Skip to content

[VarExporter] Add Hydrator::hydrate() and preserve PHP references when using it #46452

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Symfony/Component/VarExporter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
-----

Expand Down
78 changes: 78 additions & 0 deletions src/Symfony/Component/VarExporter/Hydrator.php
Original file line number Diff line number Diff line change
@@ -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 = \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;
}
}
61 changes: 14 additions & 47 deletions src/Symfony/Component/VarExporter/Instantiator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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<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
*
* // 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);
}
}
6 changes: 3 additions & 3 deletions src/Symfony/Component/VarExporter/Internal/Exporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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,
Expand Down
Loading