Nicolas Grekas
Contributed by Nicolas Grekas in #63612 , #63695 and #64049

In PHP, the native clone keyword performs a shallow copy: nested objects remain shared with the original instance. Deep cloning recursively clones the full object graph so the clone shares no references with the original.

Deep cloning in PHP has traditionally relied on unserialize(serialize($value)). Although effective, this approach is slow and memory-intensive because it breaks copy-on-write (COW) semantics by rebuilding the entire value graph from a serialized representation.

Symfony 8.1 introduces a new DeepCloner class in the VarExporter component that deep-clones PHP values while preserving COW for strings and arrays. Instead of serializing data, it reconstructs the object graph directly, making cloning significantly faster and more memory efficient.

Basic Usage

For a one-off deep clone, use the static deepClone() method:

1
2
3
use Symfony\Component\VarExporter\DeepCloner;

$clone = DeepCloner::deepClone($originalObject);

To clone the same prototype repeatedly, create a DeepCloner instance once. The object graph is analyzed upfront, making subsequent clone() calls much cheaper:

1
2
3
4
$cloner = new DeepCloner($prototype);

$clone1 = $cloner->clone();
$clone2 = $cloner->clone();

You can also clone the root object into a compatible class with cloneAs():

1
2
$childDefinition = (new DeepCloner($definition))
    ->cloneAs(ChildDefinition::class);

DeepCloner instances can also be exported to arrays and restored later, making them suitable for caching or transport across processes (json_encode(), MessagePack, APCu, OPcache-warmed .php files, etc.). The payload is typically 30-40% smaller than serialize($value):

1
2
3
4
$payload = (new DeepCloner($graph))->toArray();
$json = json_encode($payload);
// ... store, cache or send the payload ...
$clone = DeepCloner::fromArray(json_decode($json, true))->clone();

Finally, the lower-level Hydrator and Instantiator classes are deprecated in 8.1 in favor of the single deepclone_hydrate() function which instantiates and hydrates an object (including private, protected and readonly properties) in a single call:

1
2
3
4
5
6
// Before (deprecated in 8.1):
$user = Instantiator::instantiate(User::class);
Hydrator::hydrate($user, ['name' => 'Alice']);

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

Real-World Impact Inside Symfony

In benchmarks, DeepCloner consistently outperforms unserialize(serialize()): it is 4x faster for typical object graphs (100 objects with a few properties each) and up to 15x faster for graphs with many properties (50 objects with 20 properties each), while also using significantly less memory.

That's why DeepCloner is not a niche addition for VarExporter users. Symfony 8.1 now uses DeepCloner internally in several core components:

  • DependencyInjection: for cloning service definitions during container compilation;
  • FrameworkBundle: when dumping the compiled container for debugging;
  • Form: for cloning form data snapshots between requests;
  • Cache: in the ArrayAdapter implementation.

As a result, Symfony applications automatically benefit from faster container compilation, lower memory usage, and more efficient in-memory caching.

Bonus: the ext-deepclone PHP Extension

Alongside DeepCloner, the Symfony team has released a new PHP extension, symfony/php-ext-deepclone. It provides native implementations of the deepclone_to_array(), deepclone_from_array() and deepclone_hydrate() functions.

When the extension is installed, DeepCloner transparently uses it instead of the userland polyfill, providing even better performance without requiring any application changes.

Published in #Living on the edge