Skip to content

Commit f5384f9

Browse files
feature #568 [DeepClone] Add deepclone_hydrate() polyfill (nicolas-grekas)
This PR was merged into the 1.x branch. Discussion ---------- [DeepClone] Add deepclone_hydrate() polyfill Pure-PHP implementation of `deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = []): object`, matching the C extension (symfony/php-ext-deepclone#6). - `$scoped_vars` writes directly with `Closure::bind` for scope access - `$mangled_vars` resolves mangled keys (`"\0Class\0prop"`, `"\0*\0prop"`) to the correct scope - SPL special `"\0"` key: ArrayObject/ArrayIterator constructor, SplObjectStorage attach - PHP `&` reference preservation via `$object->$name = $value; $object->$name = &$value;` - Instantiability validation reuses `getClassReflector()` (same rules as `deepclone_from_array`) - `ValueError` on integer keys in `$mangled_vars` or non-array values in `$scoped_vars` - Fix: `getClassReflector()` now rejects enums - All parameters renamed to snake_case (`$allowed_classes`, `$object_or_class`, etc.) Commits ------- 626b477 [DeepClone] Add deepclone_hydrate() polyfill
2 parents 3db8459 + 626b477 commit f5384f9

6 files changed

Lines changed: 642 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.35.0
2+
3+
* Add polyfill for `deepclone_hydrate()`
4+
15
# 1.34.0
26

37
* Add polyfill for the `symfony/deepclone` extension

src/DeepClone/DeepClone.php

Lines changed: 227 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,17 @@ final class DeepClone
3333
private static array $instantiableWithoutConstructor = [];
3434
private static array $needsFullUnserialize = [];
3535
private static array $hydrators = [];
36+
private static array $simpleHydrators = [];
3637
private static array $scopeMaps = [];
3738
private static array $protos = [];
3839
private static array $classInfo = [];
3940
private static \stdClass $sentinel;
4041

4142
/**
42-
* @param list<string>|null $allowedClasses Classes that may be serialized
43-
* (null = all, [] = none)
43+
* @param list<string>|null $allowed_classes Classes that may be serialized
44+
* (null = all, [] = none)
4445
*/
45-
public static function deepclone_to_array(mixed $value, ?array $allowedClasses = null): array
46+
public static function deepclone_to_array(mixed $value, ?array $allowed_classes = null): array
4647
{
4748
if (\is_resource($value)) {
4849
throw new \DeepClone\NotInstantiableException('Type "'.get_resource_type($value).' resource" is not instantiable.');
@@ -53,11 +54,11 @@ public static function deepclone_to_array(mixed $value, ?array $allowedClasses =
5354
}
5455

5556
$allowedSet = null;
56-
if (null !== $allowedClasses) {
57+
if (null !== $allowed_classes) {
5758
$allowedSet = [];
58-
foreach ($allowedClasses as $cls) {
59+
foreach ($allowed_classes as $cls) {
5960
if (!\is_string($cls)) {
60-
throw new \ValueError('deepclone_to_array(): Argument $allowedClasses must be an array of class names, '.self::valueName($cls).' given');
61+
throw new \ValueError('deepclone_to_array(): Argument $allowed_classes must be an array of class names, '.self::valueName($cls).' given');
6162
}
6263
$allowedSet[strtolower($cls)] = true;
6364
}
@@ -242,10 +243,10 @@ public static function deepclone_to_array(mixed $value, ?array $allowedClasses =
242243
}
243244

244245
/**
245-
* @param list<string>|null $allowedClasses Classes that may be instantiated
246-
* (null = all, [] = none)
246+
* @param list<string>|null $allowed_classes Classes that may be instantiated
247+
* (null = all, [] = none)
247248
*/
248-
public static function deepclone_from_array(array $data, ?array $allowedClasses = null): mixed
249+
public static function deepclone_from_array(array $data, ?array $allowed_classes = null): mixed
249250
{
250251
if (\array_key_exists('value', $data)) {
251252
return $data['value'];
@@ -286,8 +287,8 @@ public static function deepclone_from_array(array $data, ?array $allowedClasses
286287
}
287288
$numClasses = \count($classes);
288289

289-
if (null !== $allowedClasses) {
290-
$allowed = array_change_key_case(array_flip($allowedClasses));
290+
if (null !== $allowed_classes) {
291+
$allowed = array_change_key_case(array_flip($allowed_classes));
291292
foreach ($classes as $cls) {
292293
if (!isset($allowed[strtolower($cls)])) {
293294
throw new \ValueError('deepclone_from_array(): class "'.$cls.'" is not allowed');
@@ -365,10 +366,106 @@ public static function deepclone_from_array(array $data, ?array $allowedClasses
365366
$data['refs'] ?? [],
366367
$data['mask'] ?? null,
367368
$data['refMasks'] ?? [],
368-
$allowedClasses,
369+
$allowed_classes,
369370
);
370371
}
371372

373+
public static function deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = []): object
374+
{
375+
if (\is_string($object_or_class)) {
376+
if (!\array_key_exists($object_or_class, self::$cloneable)) {
377+
self::getClassReflector($object_or_class);
378+
}
379+
$r = self::$reflectors[$object_or_class] ?? new \ReflectionClass($object_or_class);
380+
if (self::$cloneable[$object_or_class]) {
381+
$object = clone self::$prototypes[$object_or_class];
382+
} elseif (self::$instantiableWithoutConstructor[$object_or_class]) {
383+
$object = $r->newInstanceWithoutConstructor();
384+
} elseif (null === self::$prototypes[$object_or_class]) {
385+
throw new \DeepClone\NotInstantiableException('Class "'.$object_or_class.'" is not instantiable.');
386+
} elseif ($r->implementsInterface('Serializable') && !method_exists($object_or_class, '__unserialize')) {
387+
$object = unserialize('C:'.\strlen($object_or_class).':"'.$object_or_class.'":0:{}');
388+
} else {
389+
$object = unserialize('O:'.\strlen($object_or_class).':"'.$object_or_class.'":0:{}');
390+
}
391+
} else {
392+
$r = null;
393+
$object = $object_or_class;
394+
}
395+
396+
if ($mangled_vars) {
397+
$class = $object::class;
398+
$r ??= new \ReflectionClass($class);
399+
400+
foreach ($mangled_vars as $name => &$value) {
401+
if (!\is_string($name)) {
402+
throw new \ValueError('deepclone_hydrate(): Argument #3 ($mangled_vars) must have only string keys');
403+
}
404+
if ("\0" === $name) {
405+
$scoped_vars[$class][$name] = &$value;
406+
continue;
407+
}
408+
if (str_starts_with($name, "\0")) {
409+
$sep = strpos($name, "\0", 1);
410+
if (false === $sep) {
411+
continue;
412+
}
413+
$scopeName = substr($name, 1, $sep - 1);
414+
$realName = substr($name, $sep + 1);
415+
416+
if (\str_contains($realName, "\0")) {
417+
throw new \ValueError('deepclone_hydrate(): Argument #3 ($mangled_vars) contains an invalid mangled key');
418+
}
419+
420+
if ('*' === $scopeName) {
421+
$scopeName = $r->hasProperty($realName) ? $r->getProperty($realName)->class : $class;
422+
}
423+
} else {
424+
$realName = $name;
425+
$scopeName = $r->hasProperty($name) ? $r->getProperty($name)->class : $class;
426+
}
427+
428+
$scoped_vars[$scopeName][$realName] = &$value;
429+
}
430+
unset($value);
431+
}
432+
433+
$obj_class = $object::class;
434+
foreach ($scoped_vars as $scope => $properties) {
435+
if (!\is_array($properties)) {
436+
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($scoped_vars) must have only array values, %s given for key "%s"', get_debug_type($properties), $scope));
437+
}
438+
if ('stdClass' !== $scope && $scope !== $obj_class && (!is_a($obj_class, $scope, true) || interface_exists($scope, false))) {
439+
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($scoped_vars) scope "%s" is not a parent of "%s"', $scope, $obj_class));
440+
}
441+
if (isset($properties["\0"]) && \is_array($properties["\0"])) {
442+
$special = $properties["\0"];
443+
unset($properties["\0"]);
444+
445+
if ($object instanceof \SplObjectStorage) {
446+
for ($i = 0, $c = \count($special); $i + 1 < $c; $i += 2) {
447+
$object[$special[$i]] = $special[$i + 1];
448+
}
449+
} elseif ($object instanceof \ArrayObject || $object instanceof \ArrayIterator) {
450+
(new \ReflectionClass($object))->getConstructor()->invokeArgs($object, $special);
451+
}
452+
}
453+
foreach ($properties as $name => $v) {
454+
if (!\is_string($name)) {
455+
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($scoped_vars) scope "%s" must have only string keys', $scope));
456+
}
457+
if (\str_contains($name, "\0")) {
458+
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($scoped_vars) scope "%s" contains an invalid property name; use bare property names in $scoped_vars, or pass mangled keys via $mangled_vars', $scope));
459+
}
460+
}
461+
if ($properties) {
462+
(self::$simpleHydrators[$scope] ??= self::getSimpleHydrator($scope))($properties, $object);
463+
}
464+
}
465+
466+
return $object;
467+
}
468+
372469
/**
373470
* Returns a type name matching zend_zval_value_name() output for
374471
* error messages compatible with the C extension.
@@ -1002,7 +1099,7 @@ private static function getClassReflector($class, $instantiableWithoutConstructo
10021099

10031100
if ($instantiableWithoutConstructor) {
10041101
$proto = $reflector->newInstanceWithoutConstructor();
1005-
} elseif (!$isClass || $reflector->isAbstract()) {
1102+
} elseif (!$isClass || $reflector->isAbstract() || $reflector->isEnum()) {
10061103
throw new \DeepClone\NotInstantiableException('Type "'.$class.'" is not instantiable.');
10071104
} elseif ($reflector->name !== $class) {
10081105
$reflector = self::$reflectors[$name = $reflector->name] ??= self::getClassReflector($name, false, $cloneable);
@@ -1183,6 +1280,123 @@ private static function getHydrator($class)
11831280
}
11841281
};
11851282
}
1283+
1284+
private static function getSimpleHydrator(string $class): \Closure
1285+
{
1286+
$baseHydrator = self::$simpleHydrators['stdClass'] ??= static function ($properties, $object) {
1287+
foreach ($properties as $name => &$value) {
1288+
$object->$name = $value;
1289+
$object->$name = &$value;
1290+
}
1291+
};
1292+
1293+
switch ($class) {
1294+
case 'stdClass':
1295+
return $baseHydrator;
1296+
1297+
case 'TypeError':
1298+
$class = 'Error';
1299+
break;
1300+
1301+
case 'ErrorException':
1302+
$class = 'Exception';
1303+
break;
1304+
1305+
case 'SplObjectStorage':
1306+
return static function ($properties, $object) {
1307+
foreach ($properties as $name => &$value) {
1308+
if ("\0" !== $name) {
1309+
$object->$name = $value;
1310+
$object->$name = &$value;
1311+
continue;
1312+
}
1313+
for ($i = 0; $i < \count($value); ++$i) {
1314+
$object[$value[$i]] = $value[++$i];
1315+
}
1316+
}
1317+
};
1318+
}
1319+
1320+
if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
1321+
throw new \DeepClone\ClassNotFoundException('Class "'.$class.'" not found.');
1322+
}
1323+
$classReflector = new \ReflectionClass($class);
1324+
1325+
switch ($class) {
1326+
case 'ArrayIterator':
1327+
case 'ArrayObject':
1328+
$constructor = $classReflector->getConstructor()->invokeArgs(...);
1329+
1330+
return static function ($properties, $object) use ($constructor) {
1331+
foreach ($properties as $name => &$value) {
1332+
if ("\0" === $name) {
1333+
$constructor($object, $value);
1334+
} else {
1335+
$object->$name = $value;
1336+
$object->$name = &$value;
1337+
}
1338+
}
1339+
};
1340+
}
1341+
1342+
if (!$classReflector->isInternal()) {
1343+
$notByRef = new \stdClass();
1344+
foreach ($classReflector->getProperties() as $propertyReflector) {
1345+
if ($propertyReflector->isStatic()) {
1346+
continue;
1347+
}
1348+
if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) {
1349+
$notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...);
1350+
} elseif ($propertyReflector->isReadOnly()) {
1351+
$notByRef->{$propertyReflector->name} = static function ($object, $value) use ($propertyReflector) {
1352+
if (!$propertyReflector->isInitialized($object)) {
1353+
$propertyReflector->setValue($object, $value);
1354+
}
1355+
};
1356+
}
1357+
}
1358+
1359+
return (function ($properties, $object) {
1360+
$notByRef = (array) $this;
1361+
1362+
foreach ($properties as $name => &$value) {
1363+
if (!$noRef = $notByRef[$name] ?? false) {
1364+
$object->$name = $value;
1365+
$object->$name = &$value;
1366+
} elseif (true !== $noRef) {
1367+
$noRef($object, $value);
1368+
} else {
1369+
$object->$name = $value;
1370+
}
1371+
}
1372+
})->bindTo($notByRef, $class);
1373+
}
1374+
1375+
if ($classReflector->name !== $class) {
1376+
return self::$simpleHydrators[$classReflector->name] ??= self::getSimpleHydrator($classReflector->name);
1377+
}
1378+
1379+
$propertySetters = [];
1380+
foreach ($classReflector->getProperties() as $propertyReflector) {
1381+
if (!$propertyReflector->isStatic()) {
1382+
$propertySetters[$propertyReflector->name] = $propertyReflector->setValue(...);
1383+
}
1384+
}
1385+
1386+
if (!$propertySetters) {
1387+
return $baseHydrator;
1388+
}
1389+
1390+
return static function ($properties, $object) use ($propertySetters) {
1391+
foreach ($properties as $name => $value) {
1392+
if ($setValue = $propertySetters[$name] ?? null) {
1393+
$setValue($object, $value);
1394+
continue;
1395+
}
1396+
$object->$name = $value;
1397+
}
1398+
};
1399+
}
11861400
}
11871401

11881402
/**

src/DeepClone/bootstrap.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
return;
1616
}
1717

18-
if (!function_exists('deepclone_to_array')) {
19-
function deepclone_to_array(mixed $value, ?array $allowedClasses = null): array { return p\DeepClone::deepclone_to_array($value, $allowedClasses); }
20-
}
21-
if (!function_exists('deepclone_from_array')) {
22-
function deepclone_from_array(array $data, ?array $allowedClasses = null): mixed { return p\DeepClone::deepclone_from_array($data, $allowedClasses); }
18+
if (\PHP_VERSION_ID >= 80200) {
19+
require __DIR__.'/bootstrap82.php';
2320
}

src/DeepClone/bootstrap82.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
use Symfony\Polyfill\DeepClone as p;
13+
14+
if (extension_loaded('deepclone')) {
15+
return;
16+
}
17+
18+
if (!function_exists('deepclone_to_array')) {
19+
function deepclone_to_array(mixed $value, ?array $allowed_classes = null): array { return p\DeepClone::deepclone_to_array($value, $allowed_classes); }
20+
}
21+
if (!function_exists('deepclone_from_array')) {
22+
function deepclone_from_array(array $data, ?array $allowed_classes = null): mixed { return p\DeepClone::deepclone_from_array($data, $allowed_classes); }
23+
}
24+
if (!function_exists('deepclone_hydrate')) {
25+
function deepclone_hydrate(object|string $object_or_class, array $scoped_vars = [], array $mangled_vars = []): object { return p\DeepClone::deepclone_hydrate($object_or_class, $scoped_vars, $mangled_vars); }
26+
}

0 commit comments

Comments
 (0)