Thanks to visit codestin.com
Credit goes to github.com

Skip to content

symfony/php-ext-deepclone

deepclone

CI License

A PHP extension that deep-clones any serializable PHP value while preserving copy-on-write for strings and arrays — resulting in lower memory usage and better performance than unserialize(serialize()).

It works by converting the value graph to a pure-array representation (only scalars and nested arrays, no objects) and back. This array form is the wire format used by Symfony's VarExporter\DeepCloner, making the extension a transparent drop-in accelerator.

Use cases

Repeated cloning of a prototype. Calling unserialize(serialize()) in a loop allocates fresh copies of every string and array, blowing up memory. This extension preserves PHP's copy-on-write: strings and scalar arrays are shared between clones until they are actually modified.

$payload = deepclone_to_array($prototype);
for ($i = 0; $i < 1000; $i++) {
    $clone = deepclone_from_array($payload);  // fast, COW-friendly
}

OPcache-friendly cache format. The pure-array payload is suitable for var_export(). When cached in a .php file, OPcache maps it into shared memory — making the "unserialize" step essentially free:

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

// Read (OPcache serves this from SHM):
$clone = deepclone_from_array(require 'cache.php');

Serialization to any format. The array form can be passed to json_encode(), MessagePack, igbinary, APCu, or any transport that handles plain PHP arrays — without losing object identity, cycles, references, or private property state.

$payload = deepclone_to_array($graph);
$json = json_encode($payload);   // safe — no objects in the array
// ... send over the wire, store in a DB, etc.
$clone = deepclone_from_array(json_decode($json, true));

Fast object instantiation and hydration. Create objects and set their properties — including private, protected, and readonly ones — without calling their constructor, faster than Reflection:

// Set private properties via scoped array (fastest path)
$user = deepclone_hydrate(User::class, [
    User::class => ['id' => 42, 'name' => 'Alice'],
    AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
]);

// Instantiate from class name with mangled keys (same format as (array) cast)
$user = deepclone_hydrate(User::class,
    ['name' => 'Alice', 'email' => '[email protected]'],
    DEEPCLONE_HYDRATE_MANGLED_VARS,
);

// Hydrate an existing object
deepclone_hydrate($existingUser, ['User' => ['name' => 'Bob']]);

API

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

$allowed_classes restricts which classes may be serialized or deserialized (null = allow all, [] = allow none). Case-insensitive, matching unserialize()'s allowed_classes option. Closures require "Closure" in the list.

deepclone_hydrate() accepts either an object to hydrate in place or a class name to instantiate without calling its constructor. PHP & references in $vars are preserved through the hydrate.

By default, $vars is keyed by declaring class name; each value is an array of property names to values. This is the fastest path — direct slot writes, no key parsing.

$user = deepclone_hydrate(User::class, [
    User::class => ['id' => 42, 'name' => 'Alice'],
    AbstractEntity::class => ['createdAt' => new \DateTimeImmutable()],
]);

Pass DEEPCLONE_HYDRATE_MANGLED_VARS in $flags to interpret $vars as a flat mangled-key array (the same shape (array) $object produces): "\0ClassName\0prop" for private, "\0*\0prop" for protected, bare name for public/dynamic. Each key is resolved to its scope automatically.

$user = deepclone_hydrate(User::class,
    ['name' => 'Alice', "\0User\0email" => '[email protected]'],
    DEEPCLONE_HYDRATE_MANGLED_VARS,
);

$flags selects the write semantics for declared-property assignments:

Flag Semantics
0 (default) ReflectionProperty::setRawValue — bypass set hooks, type-check, respect readonly
DEEPCLONE_HYDRATE_CALL_HOOKS ReflectionProperty::setValue — invoke set hooks
DEEPCLONE_HYDRATE_NO_LAZY_INIT ReflectionProperty::setRawValueWithoutLazyInitialization — skip the lazy initializer; realize the object when the last lazy property is set
DEEPCLONE_HYDRATE_MANGLED_VARS interpret $vars as a flat mangled-key array (above)

DEEPCLONE_HYDRATE_CALL_HOOKS and DEEPCLONE_HYDRATE_NO_LAZY_INIT are mutually exclusive; MANGLED_VARS composes with either. deepclone_from_array() always uses the default setRawValue semantics, mirroring unserialize().

Forgiving payload handling

deepclone_hydrate() applies three coercions before writing each declared property, so common rehydration patterns don't trip on strict-type errors. They run under every mode unless noted:

  • Readonly idempotent skip — when the readonly slot already holds an identical value (===), the write is silently skipped. Avoids Error: Cannot modify readonly property on no-op rehydration. Different values still raise the engine's normal error.
  • nullunset() for non-nullable typed properties — writing null into a non-nullable typed slot stores the uninitialized state (so ReflectionProperty::isInitialized() returns false and reads raise the standard "must not be accessed before initialization" error) instead of throwing TypeError. This restores a state otherwise unreachable through hydration. Nullable / mixed types keep their existing semantics. Hooked properties never trigger this rule (no backing slot to "unset" semantically; a set hook may handle null itself).
  • Scalar → backed-enum cast — when the property is typed with a single (possibly nullable) backed enum and the payload value is a scalar matching the enum's backing type (int ↔ int-backed, string ↔ string-backed), the value is cast to the corresponding case. Unknown backing values raise the standard ValueError ("X is not a valid backing value for enum Y"), matching Enum::from(). Union/intersection types on the property itself are left untouched. The decision rests on the property type only — hook presence and DEEPCLONE_HYDRATE_CALL_HOOKS mode don't change it. Set hooks on enum-typed properties accordingly receive the enum case, not the raw scalar.

The special "\0" key sets the internal state of SPL classes. In scoped mode it goes inside a scope entry; in MANGLED_VARS mode it is a flat key:

// Scoped mode:
$ao = deepclone_hydrate('ArrayObject', ['ArrayObject' => ["\0" => [['x' => 1]]]]);

// MANGLED_VARS mode:
$ao = deepclone_hydrate('ArrayObject',
    ["\0" => [['x' => 1], ArrayObject::ARRAY_AS_PROPS]],
    DEEPCLONE_HYDRATE_MANGLED_VARS,
);

// SplObjectStorage: "\0" => [$obj1, $info1, $obj2, $info2, ...]
$s = deepclone_hydrate('SplObjectStorage',
    ["\0" => [$obj, 'metadata']],
    DEEPCLONE_HYDRATE_MANGLED_VARS,
);

What it preserves

  • Object identity (shared references stay shared)
  • PHP & hard references
  • Cycles in the object graph
  • Private/protected properties across inheritance
  • __serialize / __unserialize / __sleep / __wakeup semantics
  • Named closures (first-class callables like strlen(...))
  • Enum values
  • Copy-on-write for strings and scalar arrays

Error handling

Exception Thrown by When
DeepClone\NotInstantiableException deepclone_to_array, deepclone_hydrate Resource, anonymous class, Reflection*, internal class without serialization support
DeepClone\ClassNotFoundException deepclone_from_array, deepclone_hydrate Payload/class name references a class that doesn't exist
ValueError all three Malformed input, or class not in $allowed_classes

Both exception classes extend \InvalidArgumentException.

Requirements

  • PHP 8.2+ (NTS or ZTS, 64-bit and 32-bit)

Installation

With PIE (recommended)

pie install symfony/deepclone

Then enable in php.ini:

extension=deepclone

Manual build

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/var-exporter and symfony/polyfill-deepclone provide the same deepclone_to_array(), deepclone_from_array(), and deepclone_hydrate() functions in pure PHP. When this extension is loaded it replaces the polyfill transparently — no code change needed.

Symfony's Hydrator::hydrate() and Instantiator::instantiate() delegate directly to deepclone_hydrate(), making them thin one-liner wrappers.

License

Released under the MIT license.

About

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

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages