@@ -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/**
0 commit comments