Skip to content

Commit 831ba12

Browse files
[DeepClone] Add deepclone_hydrate() polyfill
Add pure-PHP implementation of deepclone_hydrate(object|string, array $properties, array $scopedProperties): object Matches the C extension (symfony/php-ext-deepclone#6): - Flat $properties with mangled key format ("\0Class\0prop", "\0*\0prop") - Pre-scoped $scopedProperties keyed by declaring class - SPL special "\0" key for ArrayObject, ArrayIterator, SplObjectStorage - PHP & reference preservation - Instantiability validation via getClassReflector() - ValueError on integer keys or non-array scoped values - Enum rejection in getClassReflector()
1 parent 3db8459 commit 831ba12

6 files changed

Lines changed: 564 additions & 50 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: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ 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 = [];
@@ -1002,7 +1003,7 @@ private static function getClassReflector($class, $instantiableWithoutConstructo
10021003

10031004
if ($instantiableWithoutConstructor) {
10041005
$proto = $reflector->newInstanceWithoutConstructor();
1005-
} elseif (!$isClass || $reflector->isAbstract()) {
1006+
} elseif (!$isClass || $reflector->isAbstract() || $reflector->isEnum()) {
10061007
throw new \DeepClone\NotInstantiableException('Type "'.$class.'" is not instantiable.');
10071008
} elseif ($reflector->name !== $class) {
10081009
$reflector = self::$reflectors[$name = $reflector->name] ??= self::getClassReflector($name, false, $cloneable);
@@ -1183,6 +1184,202 @@ private static function getHydrator($class)
11831184
}
11841185
};
11851186
}
1187+
1188+
public static function deepclone_hydrate(object|string $objectOrClass, array $properties = [], array $scopedProperties = []): object
1189+
{
1190+
if (\is_string($objectOrClass)) {
1191+
if (!\array_key_exists($objectOrClass, self::$cloneable)) {
1192+
self::getClassReflector($objectOrClass);
1193+
}
1194+
$r = self::$reflectors[$objectOrClass] ?? new \ReflectionClass($objectOrClass);
1195+
if (self::$cloneable[$objectOrClass]) {
1196+
$object = clone self::$prototypes[$objectOrClass];
1197+
} elseif (self::$instantiableWithoutConstructor[$objectOrClass]) {
1198+
$object = $r->newInstanceWithoutConstructor();
1199+
} elseif (null === self::$prototypes[$objectOrClass]) {
1200+
throw new \DeepClone\NotInstantiableException('Class "'.$objectOrClass.'" is not instantiable.');
1201+
} elseif ($r->implementsInterface('Serializable') && !method_exists($objectOrClass, '__unserialize')) {
1202+
$object = unserialize('C:'.\strlen($objectOrClass).':"'.$objectOrClass.'":0:{}');
1203+
} else {
1204+
$object = unserialize('O:'.\strlen($objectOrClass).':"'.$objectOrClass.'":0:{}');
1205+
}
1206+
} else {
1207+
$object = $objectOrClass;
1208+
}
1209+
1210+
if ($properties) {
1211+
$class = $object::class;
1212+
$r ??= new \ReflectionClass($class);
1213+
1214+
foreach ($properties as $name => &$value) {
1215+
if (!\is_string($name)) {
1216+
throw new \ValueError('deepclone_hydrate(): Argument #2 ($properties) must have only string keys');
1217+
}
1218+
if ("\0" === $name) {
1219+
$scopedProperties[$class][$name] = &$value;
1220+
continue;
1221+
}
1222+
if (\str_starts_with($name, "\0")) {
1223+
$sep = \strpos($name, "\0", 1);
1224+
if (false === $sep) {
1225+
continue;
1226+
}
1227+
$scopeName = \substr($name, 1, $sep - 1);
1228+
$realName = \substr($name, $sep + 1);
1229+
1230+
if ('*' === $scopeName) {
1231+
$scopeName = $r->hasProperty($realName) ? $r->getProperty($realName)->class : $class;
1232+
}
1233+
} else {
1234+
$realName = $name;
1235+
$scopeName = $r->hasProperty($name) ? $r->getProperty($name)->class : $class;
1236+
}
1237+
1238+
$scopedProperties[$scopeName][$realName] = &$value;
1239+
}
1240+
unset($value);
1241+
}
1242+
1243+
foreach ($scopedProperties as $scope => $properties) {
1244+
if (!\is_array($properties)) {
1245+
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #3 ($scopedProperties) must have only array values, %s given for key "%s"', get_debug_type($properties), $scope));
1246+
}
1247+
if (isset($properties["\0"]) && \is_array($properties["\0"])) {
1248+
$special = $properties["\0"];
1249+
unset($properties["\0"]);
1250+
1251+
if ($object instanceof \SplObjectStorage) {
1252+
for ($i = 0, $c = \count($special); $i + 1 < $c; $i += 2) {
1253+
$object[$special[$i]] = $special[$i + 1];
1254+
}
1255+
} elseif ($object instanceof \ArrayObject || $object instanceof \ArrayIterator) {
1256+
(new \ReflectionClass($object))->getConstructor()->invokeArgs($object, $special);
1257+
}
1258+
}
1259+
if ($properties) {
1260+
(self::$simpleHydrators[$scope] ??= self::getSimpleHydrator($scope))($properties, $object);
1261+
}
1262+
}
1263+
1264+
return $object;
1265+
}
1266+
1267+
private static function getSimpleHydrator(string $class): \Closure
1268+
{
1269+
$baseHydrator = self::$simpleHydrators['stdClass'] ??= static function ($properties, $object) {
1270+
foreach ($properties as $name => &$value) {
1271+
$object->$name = $value;
1272+
$object->$name = &$value;
1273+
}
1274+
};
1275+
1276+
switch ($class) {
1277+
case 'stdClass':
1278+
return $baseHydrator;
1279+
1280+
case 'TypeError':
1281+
$class = 'Error';
1282+
break;
1283+
1284+
case 'ErrorException':
1285+
$class = 'Exception';
1286+
break;
1287+
1288+
case 'SplObjectStorage':
1289+
return static function ($properties, $object) {
1290+
foreach ($properties as $name => &$value) {
1291+
if ("\0" !== $name) {
1292+
$object->$name = $value;
1293+
$object->$name = &$value;
1294+
continue;
1295+
}
1296+
for ($i = 0; $i < \count($value); ++$i) {
1297+
$object[$value[$i]] = $value[++$i];
1298+
}
1299+
}
1300+
};
1301+
}
1302+
1303+
if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
1304+
throw new \DeepClone\ClassNotFoundException('Class "'.$class.'" not found.');
1305+
}
1306+
$classReflector = new \ReflectionClass($class);
1307+
1308+
switch ($class) {
1309+
case 'ArrayIterator':
1310+
case 'ArrayObject':
1311+
$constructor = $classReflector->getConstructor()->invokeArgs(...);
1312+
1313+
return static function ($properties, $object) use ($constructor) {
1314+
foreach ($properties as $name => &$value) {
1315+
if ("\0" === $name) {
1316+
$constructor($object, $value);
1317+
} else {
1318+
$object->$name = $value;
1319+
$object->$name = &$value;
1320+
}
1321+
}
1322+
};
1323+
}
1324+
1325+
if (!$classReflector->isInternal()) {
1326+
$notByRef = new \stdClass();
1327+
foreach ($classReflector->getProperties() as $propertyReflector) {
1328+
if ($propertyReflector->isStatic()) {
1329+
continue;
1330+
}
1331+
if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) {
1332+
$notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...);
1333+
} elseif ($propertyReflector->isReadOnly()) {
1334+
$notByRef->{$propertyReflector->name} = static function ($object, $value) use ($propertyReflector) {
1335+
if (!$propertyReflector->isInitialized($object)) {
1336+
$propertyReflector->setValue($object, $value);
1337+
}
1338+
};
1339+
}
1340+
}
1341+
1342+
return (function ($properties, $object) {
1343+
$notByRef = (array) $this;
1344+
1345+
foreach ($properties as $name => &$value) {
1346+
if (!$noRef = $notByRef[$name] ?? false) {
1347+
$object->$name = $value;
1348+
$object->$name = &$value;
1349+
} elseif (true !== $noRef) {
1350+
$noRef($object, $value);
1351+
} else {
1352+
$object->$name = $value;
1353+
}
1354+
}
1355+
})->bindTo($notByRef, $class);
1356+
}
1357+
1358+
if ($classReflector->name !== $class) {
1359+
return self::$simpleHydrators[$classReflector->name] ??= self::getSimpleHydrator($classReflector->name);
1360+
}
1361+
1362+
$propertySetters = [];
1363+
foreach ($classReflector->getProperties() as $propertyReflector) {
1364+
if (!$propertyReflector->isStatic()) {
1365+
$propertySetters[$propertyReflector->name] = $propertyReflector->setValue(...);
1366+
}
1367+
}
1368+
1369+
if (!$propertySetters) {
1370+
return $baseHydrator;
1371+
}
1372+
1373+
return static function ($properties, $object) use ($propertySetters) {
1374+
foreach ($properties as $name => $value) {
1375+
if ($setValue = $propertySetters[$name] ?? null) {
1376+
$setValue($object, $value);
1377+
continue;
1378+
}
1379+
$object->$name = $value;
1380+
}
1381+
};
1382+
}
11861383
}
11871384

11881385
/**

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 $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); }
23+
}
24+
if (!function_exists('deepclone_hydrate')) {
25+
function deepclone_hydrate(object|string $objectOrClass, array $properties = [], array $scopedProperties = []): object { return p\DeepClone::deepclone_hydrate($objectOrClass, $properties, $scopedProperties); }
26+
}

0 commit comments

Comments
 (0)