Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 5 additions & 148 deletions src/DeepClone/DeepClone.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,26 +419,6 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
return $object;
}

// "\0" = SPL internal state (SplObjectStorage / ArrayObject / ArrayIterator).
if ($hasSpecial = \array_key_exists("\0", $vars)) {
if (!\is_array($special = $vars["\0"])) {
throw new \ValueError('deepclone_hydrate(): Argument #2 ($vars) special key must be of type array, '.self::valueName($vars["\0"]).' given');
}
if ($object instanceof \SplObjectStorage) {
for ($i = 0, $c = \count($special); $i + 1 < $c; $i += 2) {
$object[$special[$i]] = $special[$i + 1];
}
} elseif ($object instanceof \ArrayObject || $object instanceof \ArrayIterator) {
$r ??= self::$reflectors[$class] ?? new \ReflectionClass($class);
$r->getConstructor()->invokeArgs($object, $special);
} else {
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($vars) uses the special "\\0" key, which is only supported for SplObjectStorage, ArrayObject, and ArrayIterator; got "%s"', $class));
}
if (1 === \count($vars)) {
return $object;
}
}

// Look up each key in the pre-built (propertyScopes) index, which
// handles all three mangled-key shapes uniformly — bare "foo",
// "\0*\0foo", "\0Class\0foo" — with a single hash lookup. Bare
Expand All @@ -448,17 +428,14 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
//
// Scan first: if every key maps to $class, hand $vars straight
// to the $class hydrator and skip the intermediate grouping.
// When $hasSpecial is set, the "\0" key is still in $vars and we
// must go through the grouping path to skip it.
$r ??= self::$reflectors[$class] ?? new \ReflectionClass($class);
$propertyScopes = self::$propertyScopes[$class] ??= self::getPropertyScopes($r);

if (!$needsGroup = $hasSpecial) {
foreach ($vars as $name => $_) {
if (\array_key_exists($name, $propertyScopes) ? $class !== $propertyScopes[$name][0] : "\0" === ($name[0] ?? '')) {
$needsGroup = true;
break;
}
$needsGroup = false;
foreach ($vars as $name => $_) {
if (\array_key_exists($name, $propertyScopes) ? $class !== $propertyScopes[$name][0] : "\0" === ($name[0] ?? '')) {
$needsGroup = true;
break;
}
}

Expand Down Expand Up @@ -503,10 +480,6 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
$scoped[$class][$name] = &$value;
continue;
}
if ("\0" === $name) {
// Already handled above as the SPL special key.
continue;
}
// NUL-prefixed key that isn't in $propertyScopes: either malformed
// syntax, an unknown scope class, or a valid scope + unknown prop.
// Differentiate to match the ext's error messages.
Expand Down Expand Up @@ -715,19 +688,6 @@ private static function prepare($values, &$objectsPool, &$refsPool, &$objectsCou
$properties = $arrayValue;
goto prepare_value;
}
} elseif (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
[$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto);

// Re-create prototype consumed by (array) cast comparison above.
self::getClassReflector($class, self::$instantiableWithoutConstructor[$class], self::$cloneable[$class]);
} elseif ($value instanceof \SplObjectStorage && self::$cloneable[$class] && null !== $proto) {
// SplObjectStorage's Serializable interface breaks object references.
foreach (clone $value as $v) {
$properties[] = $v;
$properties[] = $value[$v];
}
$properties = ['SplObjectStorage' => ["\0" => $properties]];
$arrayValue = (array) $value;
} elseif ($value instanceof \Serializable || $value instanceof \__PHP_Incomplete_Class) {
++$objectsCount;
$objectsPool[$oid] = [$id = \count($objectsPool), serialize($value), [], 0, $value, null];
Expand Down Expand Up @@ -821,45 +781,6 @@ private static function prepare($values, &$objectsPool, &$refsPool, &$objectsCou
return $values;
}

/**
* @param \ArrayIterator|\ArrayObject $value
* @param \ArrayIterator|\ArrayObject $proto
*/
private static function getArrayObjectProperties($value, $proto): array
{
$reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject';
$reflector = self::$reflectors[$reflector] ??= self::getClassReflector($reflector);

$properties = [
$arrayValue = (array) $value,
$reflector->getMethod('getFlags')->invoke($value),
$value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator',
];

$reflector = $reflector->getMethod('setFlags');
$reflector->invoke($proto, \ArrayObject::STD_PROP_LIST);

if ($properties[1] & \ArrayObject::STD_PROP_LIST) {
$reflector->invoke($value, 0);
$properties[0] = (array) $value;
} else {
$reflector->invoke($value, \ArrayObject::STD_PROP_LIST);
$arrayValue = (array) $value;
}
$reflector->invoke($value, $properties[1]);

if ([[], 0, 'ArrayIterator'] === $properties) {
$properties = [];
} else {
if ('ArrayIterator' === $properties[2]) {
unset($properties[2]);
}
$properties = [$reflector->class => ["\0" => $properties]];
}

return [$arrayValue, $properties];
}

private static function reconstruct($prepared, $objectMeta, $numObjects, $properties, $resolve, $states, $refs, $preparedMask = null, $refMasks = [], ?array $allowedClasses = null, array $expectedStates = [])
{
$objects = [];
Expand Down Expand Up @@ -1342,24 +1263,6 @@ private static function getHydrator($class)
return $baseHydrator;
}

if ('SplObjectStorage' === $class) {
return static function ($properties, $objects) {
foreach ($properties as $name => $values) {
if ("\0" === $name) {
foreach ($values as $i => $v) {
for ($j = 0; $j < \count($v); ++$j) {
$objects[$i][$v[$j]] = $v[++$j];
}
}
continue;
}
foreach ($values as $i => $v) {
$objects[$i]->$name = $v;
}
}
};
}

if ('TypeError' === $class) {
$class = 'Error';
} elseif ('ErrorException' === $class) {
Expand All @@ -1369,23 +1272,6 @@ private static function getHydrator($class)
// self::$reflectors must be populated via getClassReflector() (companion caches), so read with ??.
$classReflector = self::$reflectors[$class] ?? new \ReflectionClass($class);

if (\in_array($class, ['ArrayIterator', 'ArrayObject'], true)) {
$constructor = $classReflector->getConstructor()->invokeArgs(...);

return static function ($properties, $objects) use ($constructor) {
foreach ($properties as $name => $values) {
if ("\0" !== $name) {
foreach ($values as $i => $v) {
$objects[$i]->$name = $v;
}
}
}
foreach ($properties["\0"] ?? [] as $i => $v) {
$constructor($objects[$i], $v);
}
};
}

if (!$classReflector->isInternal()) {
return $baseHydrator->bindTo(null, $class);
}
Expand Down Expand Up @@ -1440,20 +1326,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
if ('stdClass' === $class) {
return $baseHydrator;
}
if ('SplObjectStorage' === $class) {
return static function ($properties, $object) {
foreach ($properties as $name => &$value) {
if ("\0" !== $name) {
$object->$name = $value;
$object->$name = &$value;
continue;
}
for ($i = 0; $i < \count($value); ++$i) {
$object[$value[$i]] = $value[++$i];
}
}
};
}

if ('TypeError' === $class) {
$class = 'Error';
Expand All @@ -1464,21 +1336,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
}
$classReflector = self::$reflectors[$class] ?? new \ReflectionClass($class);

if (\in_array($class, ['ArrayIterator', 'ArrayObject'], true)) {
$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;
$object->$name = &$value;
}
}
};
}

if ($classReflector->isInternal()) {
if ($classReflector->name !== $class) {
return self::$simpleHydrators[$classReflector->name] ??= self::getSimpleHydrator($classReflector->name);
Expand Down
53 changes: 12 additions & 41 deletions tests/DeepClone/DeepCloneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1156,24 +1156,24 @@ public function testHydrateScopedStdClass()

public function testHydrateSplObjectStorage()
{
$s = new \SplObjectStorage();
// SplObjectStorage ships __serialize/__unserialize since PHP 7.4 —
// deepclone_hydrate() instantiates, the caller wires up state.
$o1 = new \stdClass();
$o2 = new \stdClass();

$result = deepclone_hydrate($s, ["\0" => [$o1, 'info1', $o2, 'info2']]);
$s = deepclone_hydrate('SplObjectStorage');
$s->__unserialize([[$o1, 'info1', $o2, 'info2'], []]);

$this->assertSame($s, $result);
$this->assertSame(2, $result->count());
$result->rewind();
$this->assertSame($o1, $result->current());
$this->assertSame('info1', $result->getInfo());
$this->assertSame(2, $s->count());
$s->rewind();
$this->assertSame($o1, $s->current());
$this->assertSame('info1', $s->getInfo());
}

public function testHydrateArrayObject()
{
$ao = deepclone_hydrate('ArrayObject', [
"\0" => [['x' => 1, 'y' => 2], \ArrayObject::ARRAY_AS_PROPS],
]);
$ao = deepclone_hydrate('ArrayObject');
$ao->__unserialize([\ArrayObject::ARRAY_AS_PROPS, ['x' => 1, 'y' => 2], []]);

$this->assertInstanceOf(\ArrayObject::class, $ao);
$this->assertSame(1, $ao['x']);
Expand All @@ -1182,9 +1182,8 @@ public function testHydrateArrayObject()

public function testHydrateArrayIterator()
{
$ai = deepclone_hydrate('ArrayIterator', [
"\0" => [['a', 'b', 'c']],
]);
$ai = deepclone_hydrate('ArrayIterator');
$ai->__unserialize([0, ['a', 'b', 'c'], []]);

$this->assertInstanceOf(\ArrayIterator::class, $ai);
$this->assertCount(3, $ai);
Expand All @@ -1200,34 +1199,6 @@ public function testHydrateFlatCaseInsensitiveClass()
$this->assertEquals((object) ['p' => 123], deepclone_hydrate('STDcLASS', ['p' => 123]));
}

public function testHydrateFlatArrayObject()
{
$this->assertEquals(new \ArrayObject([123]), deepclone_hydrate(\ArrayObject::class, ["\0" => [[123]]]));
}

public function testHydrateFlatSplObjectStorage()
{
$o1 = new \stdClass();
$s = deepclone_hydrate('SplObjectStorage', ["\0" => [$o1, 'data']]);

$this->assertSame(1, $s->count());
}

public function testHydrateScopedArrayObjectOwnClass()
{
$ao = deepclone_hydrate('ArrayObject', ["\0" => [[456]]]);

$this->assertSame(456, $ao[0]);
}

public function testHydrateScopedSplObjectStorageOwnClass()
{
$o1 = new \stdClass();
$s = deepclone_hydrate('SplObjectStorage', ["\0" => [$o1, 'info']]);

$this->assertSame(1, $s->count());
}

public function testHydrateFlatMangledKeys()
{
$expected = [
Expand Down
Loading