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]