symfony / deepclone
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
Requires
- php: >=8.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.8.5
- v0.8.4
- v0.8.3
- v0.8.2
- v0.8.1
- v0.8.0
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.1
- v0.6.0
- v0.5.1
- v0.5.0
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.1
- v0.1.0
- dev-bundled-classes
- dev-align-rejections
- dev-payload-alignment
- dev-malformed-payload-errors
- dev-spl-heaps
- dev-not-serializable
- dev-dynamic-parent-private
- dev-sleep-parent-private
- dev-php-8.6
- dev-clean-masks
- dev-dynamic-property-refs
- dev-lazy-proxy-instances
- dev-native-constexpr-ids
This package is auto-updated.
Last update: 2026-09-26 05:44:45 UTC
README
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, likeclonedoes and regardless ofSKIP_INITIALIZATION_ON_SERIALIZE, and a lazy proxy comes back as an instance of its own class, as withunserialize(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.