symfony/polyfill-deepclone
Pure-PHP implementation of the deepclone extension's functions for converting value graphs to arrays and back, preserving object identity, references, cycles, and private state—faster and more memory-efficient than serialize/unserialize.
Installation:
composer require symfony/polyfill-deepclone:^1.43
No additional configuration is required—it works as a drop-in replacement for the native deepclone extension. Pin to ^1.43 if you rely on strict malformed-payload reporting (see Gotchas).
First Use Case: Convert an object to a serializable array and back:
use Symfony\Polyfill\DeepClone\deepclone;
$original = new class {
public $name = "Test";
private $secret = "hidden";
public $nested = ["a" => 1, "b" => new stdClass()];
};
// Convert to array (preserves structure, no objects)
$array = deepclone_to_array($original);
// Rebuild object (preserves private state, cycles, references)
$cloned = deepclone_from_array($array);
Where to Look First:
deepclone_to_array() and deepclone_from_array() in the Symfony Polyfill README.NotInstantiableException and ClassNotFoundException for error handling, plus the malformed-payload errors introduced in v1.43.2.Deep Cloning Objects with Private State:
$user = new User(['name' => 'John', 'email' => '[email protected]']);
$clonedUser = deepclone_from_array(deepclone_to_array($user));
// Private properties and methods are preserved
assert($clonedUser->getPrivateData() === $user->getPrivateData());
Handling Circular References:
$parent = new ParentModel();
$child = new ChildModel();
$parent->children[] = $child;
$child->parent = $parent; // Circular reference
$array = deepclone_to_array($parent);
$clonedParent = deepclone_from_array($array);
// Circularity is preserved without infinite recursion
Performance Optimization with Flags (PHP 8.3+):
// Preserve references for shared state (e.g., DTOs)
$dto = new DataTransferObject();
$clonedDto = deepclone($dto, DEEPCLONE_HYDRATE_PRESERVE_REFS);
Round-Trip Cost Profile:
deepclone_from_array() is measurably faster, so the hydrate half of the round trip is cheaper than it used to be. The export half (deepclone_to_array()) is unchanged—if you're profiling, start there.Eloquent Model Cloning:
$user = User::with('posts')->find(1);
$clonedUser = deepclone_from_array(deepclone_to_array($user));
// Relationships and accessors are preserved
assert(count($clonedUser->posts) === count($user->posts));
Request Object Cloning for Middleware:
$request = app(Request::class);
$clonedRequest = deepclone_from_array(deepclone_to_array($request));
// Useful for testing or parallel processing
Caching Complex Objects:
$cacheKey = 'complex_object_' . md5(serialize($object));
$cachedArray = deepclone_to_array($object);
cache()->put($cacheKey, $cachedArray, now()->addHours(1));
// Later... treat the payload as untrusted input
try {
$restoredObject = deepclone_from_array(cache()->get($cacheKey));
} catch (Throwable $e) {
cache()->forget($cacheKey); // Stale/corrupt entry — rebuild from source
$restoredObject = rebuildObject();
}
Isolated Test Data:
$original = new ExpensiveModel();
$testData = deepclone_from_array(deepclone_to_array($original));
// Safe to modify $testData without affecting $original
Asserting Deep Clones:
use Symfony\Polyfill\DeepClone\deepclone;
$original = new class { public $value = 42; };
$cloned = deepclone($original);
$this->assertNotSame($original, $cloned);
$this->assertEquals($original->value, $cloned->value);
Testing Failure Paths:
expectException() tests against hand-crafted broken payloads, and to have them pass identically on PHP 8.3+ native deepclone.Malformed Payloads Now Throw (v1.43.2):
deepclone_from_array() now reports malformed payloads like the native extension instead of tolerating them. Anything that persists a payload and restores it later—cache, queues, database columns, files—should be treated as untrusted input and wrapped:
try {
$cloned = deepclone_from_array($payload);
} catch (Throwable $e) {
// Corrupt/legacy payload: invalidate and rebuild
}
SPL \0 Keys:
\0 keys by default (fixed in v1.37.0). If your code relies on these, ensure they’re handled upstream (e.g., normalize keys before cloning).Metadata Limits:
try {
$array = deepclone_to_array($hugeObject);
} catch (Error $e) {
if (str_contains($e->getMessage(), 'IS_LONG objectMeta count')) {
// Handle or split the object graph
}
}
Deprecated Scoped Mode:
// Old (deprecated)
deepclone($obj, DeepClone::SCOPED);
// New (only option)
deepclone($obj, DeepClone::MANGLED);
Non-Serializable Objects:
__serialize()/__unserialize() will throw NotInstantiableException. Handle gracefully:
try {
$cloned = deepclone_from_array($array);
} catch (NotInstantiableException $e) {
// Fallback or log
}
Verify Array Representation:
deepclone_to_array() to debug issues:
$array = deepclone_to_array($object);
var_export($array); // Check for missing/incorrect data
Reproduce Malformed-Payload Failures Deliberately:
$stored = cache()->get($key);
$expected = deepclone_to_array($object);
// Any structural drift (truncated write, JSON-ish cache driver, size cap) shows up here
dump(array_diff_key($expected, $stored), array_diff_key($stored, $expected));
serialize()-safe drivers for raw arrays.Check for Cycles:
var_export() to dump the array to a .php file and inspect cycles:
file_put_contents('debug_clone.php', '<?php return ' . var_export($array, true) . ';');
Performance Profiling:
deepclone (PHP 8.3+), and time the two halves separately—restore got faster in v1.43.2, export did not:
$t = microtime(true); $array = deepclone_to_array($obj); $exportMs = (microtime(true) - $t) * 1000;
$t = microtime(true); $cloned = deepclone_from_array($array); $importMs = (microtime(true) - $t) * 1000;
Custom Allowed Classes:
$array = deepclone_to_array($obj, [User::class, Post::class]);
Post-Processing:
$array = deepclone_to_array($obj);
$array['modified'] = true; // Add/remove data
$cloned = deepclone_from_array($array);
Integration with Symfony VarExporter:
use Symfony\Component\VarExporter\VarExporter;
$exported = VarExporter::export($obj, VarExporter::EXPORT_DEEP_CLONE);
$cloned = deepclone_from_array(eval('return ' . $exported . ';'));
Eloquent Relationships:
$user = User::with('posts.comments')->find(1);
$cloned = deepclone_from_array(deepclone_to_array($user));
Request Objects:
Illuminate\Http\Request may not preserve uploaded files. Handle separately:
$requestData = deepclone_to_array($request->all());
$clonedRequest = new Request($requestData);
Service Container:
// Bad: Cloning a singleton
$clonedRepo = deepclone($this->app->make(Repository::class));
// Good: Re-instantiate
$clonedRepo = $this->app->make(Repository::class);
Queued Jobs and Cached Payloads:
How can I help you explore Laravel packages today?