Skip to content

Commit a7fe0e4

Browse files
[VarExporter] Add Hydrator::hydrate() and preserve PHP references when using it
1 parent 0f2b2a7 commit a7fe0e4

File tree

7 files changed

+275
-72
lines changed

7 files changed

+275
-72
lines changed

src/Symfony/Component/VarExporter/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
CHANGELOG
22
=========
33

4+
6.2
5+
---
6+
7+
* Add `Hydrator::hydrate()`
8+
* Preserve PHP references also when using `Hydrator::hydrate()` or `Instantiator::instantiate()`
9+
* Add support for hydrating from native (array) casts
10+
411
5.1.0
512
-----
613

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\VarExporter;
13+
14+
use Symfony\Component\VarExporter\Internal\Hydrator as InternalHydrator;
15+
16+
/**
17+
* Utility class to hydrate the properties of an object.
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
final class Hydrator
22+
{
23+
/**
24+
* Sets the properties of an object without calling its constructor nor any other methods.
25+
*
26+
* For example:
27+
*
28+
* // Sets the public or protected $object->propertyName property
29+
* Hydrator::hydrate($object, ['propertyName' => $propertyValue]);
30+
*
31+
* // Sets a private property defined on its parent Bar class:
32+
* Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]);
33+
*
34+
* // Alternative way to set the private $object->privateBarProperty property
35+
* Hydrator::hydrate($object, [], [
36+
* Bar::class => ['privateBarProperty' => $propertyValue],
37+
* ]);
38+
*
39+
* Instances of ArrayObject, ArrayIterator and SplObjectHash can be hydrated
40+
* by using the special "\0" property name to define their internal value:
41+
*
42+
* // creates an SplObjectHash where $info1 is attached to $obj1, etc.
43+
* Hydrator::hydrate($object, ["\0" => [$obj1, $info1, $obj2, $info2...]]);
44+
*
45+
* // creates an ArrayObject populated with $inputArray
46+
* Hydrator::hydrate($object, ["\0" => [$inputArray]]);
47+
*
48+
* @template T of object
49+
*
50+
* @param T $instance The object to hydrate
51+
* @param array<string, mixed> $properties The properties to set on the instance
52+
* @param array<class-string, array<string, mixed>> $scopedProperties The properties to set on the instance,
53+
* keyed by their declaring class
54+
*
55+
* @return T
56+
*/
57+
public static function hydrate(object $instance, array $properties = [], array $scopedProperties = []): object
58+
{
59+
if ($properties) {
60+
$class = \get_class($instance);
61+
$propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class);
62+
63+
foreach ($properties as $name => &$value) {
64+
[$scope, $name] = $propertyScopes[$name] ?? [$class, $name];
65+
$scopedProperties[$scope][$name] = &$value;
66+
}
67+
unset($value);
68+
}
69+
70+
foreach ($scopedProperties as $class => $properties) {
71+
if ($properties) {
72+
(InternalHydrator::$simpleHydrators[$class] ??= InternalHydrator::getSimpleHydrator($class))($properties, $instance);
73+
}
74+
}
75+
76+
return $instance;
77+
}
78+
}

src/Symfony/Component/VarExporter/Instantiator.php

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Component\VarExporter\Exception\ExceptionInterface;
1515
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
16-
use Symfony\Component\VarExporter\Internal\Hydrator;
1716
use Symfony\Component\VarExporter\Internal\Registry;
1817

1918
/**
@@ -26,67 +25,35 @@ final class Instantiator
2625
/**
2726
* Creates an object and sets its properties without calling its constructor nor any other methods.
2827
*
29-
* For example:
28+
* @see Hydrator::hydrate() for examples
3029
*
31-
* // creates an empty instance of Foo
32-
* Instantiator::instantiate(Foo::class);
30+
* @template T of object
3331
*
34-
* // creates a Foo instance and sets one of its properties
35-
* Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]);
32+
* @param class-string<T> $class The class of the instance to create
33+
* @param array<string, mixed> $properties The properties to set on the instance
34+
* @param array<class-string, array<string, mixed>> $scopedProperties The properties to set on the instance,
35+
* keyed by their declaring class
3636
*
37-
* // creates a Foo instance and sets a private property defined on its parent Bar class
38-
* Instantiator::instantiate(Foo::class, [], [
39-
* Bar::class => ['privateBarProperty' => $propertyValue],
40-
* ]);
41-
*
42-
* Instances of ArrayObject, ArrayIterator and SplObjectHash can be created
43-
* by using the special "\0" property name to define their internal value:
44-
*
45-
* // creates an SplObjectHash where $info1 is attached to $obj1, etc.
46-
* Instantiator::instantiate(SplObjectStorage::class, ["\0" => [$obj1, $info1, $obj2, $info2...]]);
47-
*
48-
* // creates an ArrayObject populated with $inputArray
49-
* Instantiator::instantiate(ArrayObject::class, ["\0" => [$inputArray]]);
50-
*
51-
* @param string $class The class of the instance to create
52-
* @param array $properties The properties to set on the instance
53-
* @param array $privateProperties The private properties to set on the instance,
54-
* keyed by their declaring class
37+
* @return T
5538
*
5639
* @throws ExceptionInterface When the instance cannot be created
5740
*/
58-
public static function instantiate(string $class, array $properties = [], array $privateProperties = []): object
41+
public static function instantiate(string $class, array $properties = [], array $scopedProperties = []): object
5942
{
60-
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
43+
$reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class);
6144

6245
if (Registry::$cloneable[$class]) {
63-
$wrappedInstance = [clone Registry::$prototypes[$class]];
46+
$instance = clone Registry::$prototypes[$class];
6447
} elseif (Registry::$instantiableWithoutConstructor[$class]) {
65-
$wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
48+
$instance = $reflector->newInstanceWithoutConstructor();
6649
} elseif (null === Registry::$prototypes[$class]) {
6750
throw new NotInstantiableTypeException($class);
6851
} elseif ($reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize')) {
69-
$wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')];
52+
$instance = unserialize('C:'.\strlen($class).':"'.$class.'":0:{}');
7053
} else {
71-
$wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];
72-
}
73-
74-
if ($properties) {
75-
$privateProperties[$class] = isset($privateProperties[$class]) ? $properties + $privateProperties[$class] : $properties;
76-
}
77-
78-
foreach ($privateProperties as $class => $properties) {
79-
if (!$properties) {
80-
continue;
81-
}
82-
foreach ($properties as $name => $value) {
83-
// because they're also used for "unserialization", hydrators
84-
// deal with array of instances, so we need to wrap values
85-
$properties[$name] = [$value];
86-
}
87-
(Hydrator::$hydrators[$class] ?? Hydrator::getHydrator($class))($properties, $wrappedInstance);
54+
$instance = unserialize('O:'.\strlen($class).':"'.$class.'":0:{}');
8855
}
8956

90-
return $wrappedInstance[0];
57+
return Hydrator::hydrate($instance, $properties, $scopedProperties);
9158
}
9259
}

src/Symfony/Component/VarExporter/Internal/Exporter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
7272
}
7373

7474
$class = \get_class($value);
75-
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
75+
$reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class);
7676

7777
if ($reflector->hasMethod('__serialize')) {
7878
if (!$reflector->getMethod('__serialize')->isPublic()) {
@@ -108,7 +108,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
108108
$arrayValue = (array) $value;
109109
} elseif ($value instanceof \Serializable
110110
|| $value instanceof \__PHP_Incomplete_Class
111-
|| PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod
111+
|| \PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod
112112
) {
113113
++$objectsCount;
114114
$objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0];
@@ -372,7 +372,7 @@ private static function exportHydrator(Hydrator $value, string $indent, string $
372372
private static function getArrayObjectProperties($value, $proto): array
373373
{
374374
$reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject';
375-
$reflector = Registry::$reflectors[$reflector] ?? Registry::getClassReflector($reflector);
375+
$reflector = Registry::$reflectors[$reflector] ??= Registry::getClassReflector($reflector);
376376

377377
$properties = [
378378
$arrayValue = (array) $value,

0 commit comments

Comments
 (0)