Search by

symfony / deepclone

fabpotnicolas-grekas

Export any serializable PHP values as pure arrays - accelerator for Symfony's DeepCloner

Package info

github.com/symfony/php-ext-deepclone

Language:C

Type:php-ext

Ext name:ext-deepclone

pkg:composer/symfony/deepclone

Statistics

Installs: 16 177

Dependents: 0

Suggesters: 0

Stars: 17

Open Issues: 9

v0.8.5 2026-09-23 15:41 UTC

README

CI License

This extension deep-clones any serializable PHP value, faster and with less memory than unserialize(serialize()): strings and arrays keep their copy-on-write semantics.

It turns the value into a payload made of scalars and nested arrays only, and back. That's the format of Symfony's DeepCloner, which uses the extension when it's loaded and its polyfill otherwise.

Use cases

Cloning a prototype many times, with strings and arrays shared until they're modified:

$payload = deepclone_to_array($prototype);

for ($i = 0; $i < 1000; ++$i) {
    $clone = deepclone_from_array($payload);
}

Caching with OPcache: dump the payload into a .php file with var_export(), and loading it becomes essentially free since OPcache serves it from shared memory:

file_put_contents('cache.php', '<?php return '.var_export(deepclone_to_array($graph), true).';');

$clone = deepclone_from_array(require 'cache.php');

Sending a graph through JSON, MessagePack, APCu or any transport that handles plain arrays, while keeping object identities, cycles, references and private state:

$json = json_encode(deepclone_to_array($graph));

$clone = deepclone_from_array(json_decode($json, true));

Creating and hydrating objects without calling their constructor, private, protected and readonly properties included, faster than Reflection:

$user = deepclone_hydrate(User::class, ['id' => 42, 'name' => 'Alice']);

deepclone_hydrate($user, ['name' => 'Bob']);

API

function deepclone_to_array(mixed $value, ?array $allowed_classes = null, bool $allow_named_closures = false): array;
function deepclone_from_array(array $data, ?array $allowed_classes = null, bool $allow_named_closures = false): mixed;
function deepclone_hydrate(object|string $object_or_class, array $vars = [], int $flags = 0): object;

$allowed_classes works like the allowed_classes option of unserialize(), with null allowing any class. Names are case-insensitive.

$allow_named_closures allows encoding closures over named callables by name, eg strlen(...) or $obj->method(...). Both ends must enable it: resolving such a payload can create a closure over any function or method of that name, system() included, so do this only between ends that trust each other. Closures declared in constant expressions don't need it, eg #[When(self::isStrict(...))]: they're encoded as a reference to their declaration site, which resolves only to what the class itself declares.

Lazy hydration

Resolving closures is where hydration time goes. That's why, on PHP 8.4+, deepclone_from_array() creates the objects whose properties or __unserialize() state hold closures as lazy ghosts, hydrated when first used. All objects exist when the call returns, with their identities and references. The other objects are hydrated right away, and so are instances of internal classes or of classes that declare no properties: graphs without closures pay nothing for this.

The rules of lazy objects apply, with a few consequences:

  • malformed payloads and disallowed classes are still rejected right away, but errors that depend on values, like a class or enum case that doesn't exist anymore, are thrown on first use, and again on each retry;
  • __wakeup() and __unserialize() of such objects are called when they're initialized;
  • writing through a shared & reference checks the types of the objects already hydrated only - the others throw when first used if the value doesn't fit them;
  • the payload stays in memory until the last lazy object is initialized or destructed.

On closure-heavy graphs of 20k objects, this makes cloning 4-6x faster and the copy 2-3x lighter while it's not used. Using all its objects costs about the same in total.

Hydration

deepclone_hydrate() hydrates an object, or instantiates a class without calling its constructor. $vars uses the keys of (array) $object:

Key Property
"name" public, protected, or private declared by the object's class
"\0*\0name" protected
"\0Parent\0name" private declared by Parent, the object's class or a parent of it

$flags selects how properties are written:

Flag Writes
0 like ReflectionProperty::setRawValue(), without set hooks
DEEPCLONE_HYDRATE_CALL_HOOKS like ReflectionProperty::setValue(), running set hooks
DEEPCLONE_HYDRATE_NO_LAZY_INIT like ReflectionProperty::setRawValueWithoutLazyInitialization()
DEEPCLONE_HYDRATE_PRESERVE_REFS keeping the & references of $vars, where PHP allows them

CALL_HOOKS and NO_LAZY_INIT are mutually exclusive. PRESERVE_REFS isn't the default because finding references costs more than hydrating most objects. deepclone_from_array() writes like the default, as unserialize() does.

deepclone_hydrate() also forgives what rehydrating often trips on: null leaves a non-nullable typed property uninitialized instead of throwing, a scalar written to a property typed with a backed enum becomes the matching case, and writing the value a readonly property already holds does nothing.

For ArrayObject, SplObjectStorage and the other classes that keep their state internally, call __unserialize() after instantiating them, or use deepclone_from_array(), which does it for you:

$ao = deepclone_hydrate(ArrayObject::class);
$ao->__unserialize([ArrayObject::ARRAY_AS_PROPS, ['x' => 1], []]);

What it preserves

  • object identities, cycles and & references, between array elements and properties alike;
  • private and protected properties across inheritance;
  • the semantics of __serialize(), __unserialize(), __sleep() and __wakeup();
  • enums, and closures as described above;
  • the state of lazy objects: deepclone_to_array() initializes them first, like clone does and regardless of SKIP_INITIALIZATION_ON_SERIALIZE, and a lazy proxy comes back as an instance of its own class, as with unserialize(serialize());
  • copy-on-write for strings and arrays of scalars.

Errors

deepclone_to_array() and deepclone_hydrate() throw DeepClone\NotInstantiableException for resources, anonymous classes, Reflection* and internal classes that can't be serialized. deepclone_from_array() and deepclone_hydrate() throw DeepClone\ClassNotFoundException for classes that don't exist. Both extend InvalidArgumentException. Malformed input and classes missing from $allowed_classes throw ValueError.

Installation

The extension requires PHP 8.2+, NTS or ZTS, 32 or 64-bit. Install it with PIE, then add extension=deepclone to your php.ini if PIE didn't:

pie install symfony/deepclone

Or build it:

git clone https://github.com/symfony/php-ext-deepclone.git
cd php-ext-deepclone
phpize && ./configure --enable-deepclone && make && make test
sudo make install

With Symfony

symfony/polyfill-deepclone provides the same functions in pure PHP, and the extension takes over when it's loaded, with no code change. VarExporter's DeepCloner builds on them, and its Hydrator::hydrate() and Instantiator::instantiate() are one-line wrappers around deepclone_hydrate().

License

Released under the MIT license.