Mastering __clone, Deep Copy and Shallow Copy
The clone keyword in PHP produces only a shallow copy by default, nested objects remain shared. With the __clone magic method, object cloning can be extended deliberately into a deep copy, including readonly properties and the classic Prototype pattern as a creation strategy.
Table of Contents
- 1. Why object cloning deserves its own topic
- 2. The clone keyword and the default shallow copy
- 3. The __clone magic method: controlling deep copy deliberately
- 4. Correctly cloning nested objects and arrays
- 5. Object cloning and readonly properties since PHP 8.1
- 6. The Prototype pattern: cloning as a creation strategy
- 7. Resources, references and the limits of cloning
- 8. Common mistakes with object cloning
- 9. Shallow copy versus deep copy compared
- 10. Summary
- 11. FAQ
1. Why object cloning deserves its own topic
Object cloning in PHP sounds at first like a minor detail, simply duplicating an object with the clone keyword. In reality, object cloning is one of the places in the language design where subtle, hard to find bugs hide, because the default behavior is not what most developers intuitively expect. Anyone who clones an object with nested object properties and assumes both copies are fully independent gets an unpleasant surprise as soon as a change on the copy also becomes visible on the original.
The reason lies in how PHP references objects internally. A PHP variable holding an object stores an object handle, not a real value in the classic sense. Object cloning creates a new, independent instance with the same property values, but if a property is itself an object, only the handle is copied, not the referenced object. This exact behavior, called a shallow copy, is the starting point for everything else in this article.
In practice, object cloning mainly affects value objects, configuration objects, and the Prototype pattern, where a preconfigured object serves as a template for many variants. Anyone who does not deliberately control object cloning in PHP risks shared, unexpectedly mutated state between objects that are supposed to be independent in every one of these use cases.
2. The clone keyword and the default shallow copy
The clone keyword creates a new instance of the same class and copies all properties one to one onto the new instance. For scalar values like strings, integers and booleans this works exactly as expected, because PHP copies these values by value anyway. It becomes problematic as soon as a property is itself an object. In that case, object cloning only copies the reference to the nested object, not the object itself. Original and clone then point to the same inner object.
This behavior is called a shallow copy, a flat copy that only truly duplicates the top level of the object structure. For classes without object properties, a shallow copy is sufficient and even more performant, since no additional copying is needed for nested structures. For classes with mutable, nested objects, however, a shallow copy is almost always a bug that only surfaces in production, when two supposedly independent objects start affecting each other.
<?php
declare(strict_types=1);
final class Address
{
public function __construct(
public string $city,
) {
}
}
final class Customer
{
public function __construct(
public string $name,
public Address $address,
) {
}
}
$original = new Customer('Jane Doe', new Address('Berlin'));
// Default clone is a shallow copy: only the top-level object is duplicated
$copy = clone $original;
$copy->name = 'John Doe';
$copy->address->city = 'Hamburg';
echo $original->name; // Jane Doe — top-level property is independent
echo $original->address->city; // Hamburg — nested object is SHARED, not copied!
This example shows the core of the problem: $copy->name changes independently of the original because name is a scalar. $copy->address->city, however, also changes $original->address->city, because both objects reference the same Address instance. This exact behavior of object cloning is something every PHP developer needs to know before using clone on a class with object properties.
3. The __clone magic method: controlling deep copy deliberately
PHP offers the magic method __clone() for exactly this problem. It is called automatically right after PHP has created the shallow copy, but before the caller gets access to the new instance. Inside __clone(), $this is already the new, cloned instance, while the referenced object properties still point to the old, shared references at this point. The job of __clone() is to replace these shared references with its own copies, deliberately turning the automatic shallow copy into a deep copy.
The usual approach inside __clone() is to treat every object property with clone again: $this->address = clone $this->address;. This works recursively, meaning that if Address itself contains further nested objects, Address also needs its own __clone() method so the deep copy reaches all the way down to the lowest level. Object cloning is therefore not a one time operation, it has to be implemented consistently at every affected level of the object structure.
<?php
declare(strict_types=1);
final class Address
{
public function __construct(
public string $city,
) {
}
}
final class Customer
{
public function __construct(
public string $name,
public Address $address,
) {
}
/**
* Deep-copies nested objects when this instance is cloned.
*/
public function __clone(): void
{
$this->address = clone $this->address;
}
}
$original = new Customer('Jane Doe', new Address('Berlin'));
$copy = clone $original;
$copy->address->city = 'Hamburg';
echo $original->address->city; // Berlin — nested object is now truly independent
echo $copy->address->city; // Hamburg
With this __clone() implementation, the automatic object cloning turns into a genuine deep copy for the Address property. The rest of the class stays unchanged, only this one line inside __clone() decides the entire copy behavior. It is important to consciously ask, for every new object property added to the class, whether it needs to be added to __clone() as well.
4. Correctly cloning nested objects and arrays
Arrays behave fundamentally differently from objects during object cloning, because PHP arrays work on the copy on write principle and are copied like a real value type on assignment. An array of scalar values is therefore automatically duplicated correctly when the parent class is cloned, without any __clone() at all. It becomes problematic only once the array itself contains objects, because then PHP copies the array structure but the contained object references remain shared, exactly the same behavior as with a single object property.
For an array of objects, __clone() therefore needs to iterate over the array and clone each contained object individually. This pattern shows up frequently with aggregate objects, for example an Order class with an array of OrderLine objects. Object cloning an Order without handling the array properly would result in the clone and the original sharing the same OrderLine instances, even though the order itself is supposed to be independent.
<?php
declare(strict_types=1);
final class OrderLine
{
public function __construct(
public string $sku,
public int $quantity,
) {
}
}
final class Order
{
/**
* @param array<int, OrderLine> $lines
*/
public function __construct(
public string $orderNumber,
public array $lines,
) {
}
/**
* Deep-copies every object contained in the lines array.
*/
public function __clone(): void
{
$this->lines = array_map(
static fn (OrderLine $line): OrderLine => clone $line,
$this->lines,
);
}
}
$original = new Order('ORD-1001', [new OrderLine('SKU-1', 2)]);
$copy = clone $original;
$copy->lines[0]->quantity = 99;
echo $original->lines[0]->quantity; // 2 — array elements were cloned individually
echo $copy->lines[0]->quantity; // 99
This combination of array_map() and clone inside __clone() is the standard pattern for object cloning with arrays of objects. For very large arrays, it is worth keeping an eye on the cost of this deep copy, since every element is cloned individually and, with thousands of entries, a noticeable, though usually still acceptable, overhead results.
5. Object cloning and readonly properties since PHP 8.1
Readonly properties, available since PHP 8.1, change the rules for object cloning in one important detail. A readonly property may no longer be assigned directly after initialization in the constructor, not even inside __clone(), with one exception: PHP 8.3 relaxed the rule so that a readonly property may be reassigned inside __clone(), as long as it was already initialized before. Before PHP 8.3, a deep copy of readonly object properties had to be achieved by instantiating a completely new object instead, since a direct reassignment triggered an Error.
For projects that still need to support PHP 8.1 or 8.2, object cloning with readonly properties in practice means either building a completely new instance through the constructor, or foregoing readonly for object properties that need to be deep copied, and instead only declaring the primitive values as readonly. From PHP 8.3 onward this simplifies considerably, since __clone() may reassign readonly properties as usual.
<?php
declare(strict_types=1);
final class Money
{
public function __construct(
public readonly int $cents,
public readonly string $currency,
) {
}
}
final class Invoice
{
public function __construct(
public readonly string $invoiceNumber,
public readonly Money $total,
) {
}
/**
* Since PHP 8.3, readonly properties may be reassigned inside __clone().
*/
public function __clone(): void
{
// Money has no mutable nested state, but reassignment illustrates the rule
$this->total = new Money($this->total->cents, $this->total->currency);
}
}
$original = new Invoice('INV-2026-01', new Money(9900, 'EUR'));
$copy = clone $original;
var_dump($original->total === $copy->total); // false — genuinely separate instances
The key takeaway for object cloning with readonly properties: before PHP 8.3, reassignment inside __clone() is forbidden for already initialized readonly properties, from PHP 8.3 onward it is allowed. When writing new libraries, it pays off to explicitly check the supported minimum version before relying on this behavior.
6. The Prototype pattern: cloning as a creation strategy
The Prototype pattern deliberately uses object cloning as a creation strategy, instead of always building objects through a constructor with many parameters. The idea: a preconfigured prototype object is created once, and every additional instance needed is produced by clone-ing the prototype, followed by targeted adjustments. This pays off especially when object creation itself is expensive, for example because a constructor performs costly computations, while simply copying an already finished object is comparatively cheap.
A typical example is a document template system, where a base template with standard formatting is created once and cloned for every new document, instead of rebuilding the entire formatting each time. Object cloning within the Prototype pattern requires a consistent __clone() implementation, because a template typically contains nested formatting objects that must not be shared across all documents.
<?php
declare(strict_types=1);
final class DocumentStyle
{
public function __construct(
public string $fontFamily = 'Arial',
public int $fontSize = 12,
) {
}
}
final class DocumentTemplate
{
public function __construct(
public string $title,
public DocumentStyle $style,
) {
}
public function __clone(): void
{
$this->style = clone $this->style;
}
}
// Prototype: built once with the expensive default configuration
$prototype = new DocumentTemplate('Untitled', new DocumentStyle());
// Every new document clones the prototype instead of rebuilding it from scratch
$invoiceDoc = clone $prototype;
$invoiceDoc->title = 'Invoice';
$invoiceDoc->style->fontSize = 10;
$contractDoc = clone $prototype;
$contractDoc->title = 'Contract';
echo $prototype->style->fontSize; // 12 — prototype itself remains untouched
The Prototype pattern is closely related to object cloning but not identical to it. Object cloning is the technical mechanism, the Prototype pattern is the deliberate design decision to use this mechanism as the primary creation path for a family of similar objects, instead of only using it occasionally for individual copies.
7. Resources, references and the limits of cloning
Not every state can be meaningfully cloned. Resource types like open database connections, file handles or network sockets should never simply be copied inside __clone(), because two objects sharing the same underlying connection can interfere with each other on close or under concurrent access. For such cases, the right strategy is usually to build a completely new connection inside __clone(), or, more often, to explicitly remove the resource property from the clone and recreate it on demand instead of carrying it over automatically.
Another limit concerns objects deliberately designed as singletons, for example a central logger or configuration object. For such classes, object cloning is usually undesirable, because a clone contradicts the singleton idea, there would then be two independent instances where only one was ever meant to exist. PHP allows forbidding clone for a class entirely by having __clone() throw an exception.
<?php
declare(strict_types=1);
final class AppConfig
{
private static ?self $instance = null;
private function __construct(
public readonly array $settings,
) {
}
public static function instance(): self
{
return self::$instance ??= new self(['env' => 'production']);
}
/**
* Explicitly forbids cloning to preserve the singleton guarantee.
*/
public function __clone(): void
{
throw new LogicException('AppConfig must not be cloned.');
}
}
$config = AppConfig::instance();
// clone $config; // throws LogicException: AppConfig must not be cloned.
This defensive use of __clone() is just as important as its deep copy use. Deliberately forbidding object cloning is a legitimate design choice as soon as a clone would violate the invariants of a class.
8. Common mistakes with object cloning
The most common mistake is simply forgetting __clone() even though the class contains object properties. The bug often does not show up immediately, but only weeks later, when two supposedly independent objects suddenly share the same state and nobody remembers why. A second mistake is an incomplete deep copy, where __clone() exists but only handles the first level, while more deeply nested objects remain shared because the corresponding inner class does not implement __clone() itself.
A third, more subtle mistake concerns arrays with mixed content, partly scalars, partly objects. If you assume across the board that an array is always fully copied during object cloning, it is easy to overlook the contained object references. PHPStan cannot automatically detect such cases, so a manual code review of every __clone() method against the class's actual property list remains necessary, especially after adding new properties.
9. Shallow copy versus deep copy compared
The choice between shallow copy and deep copy during object cloning depends on the concrete state of the class, not on a general preference. The following table summarizes the most important differences.
| Scenario | Shallow copy (default) | Deep copy (with __clone) | Recommendation |
|---|---|---|---|
| Scalar properties only | Sufficient | Unnecessary | No __clone() needed |
| Nested, mutable objects | Risky, shared state | Correct | __clone() with clone per property |
| Array of objects | Elements stay shared | Correct | array_map with clone inside __clone() |
| Immutable value objects | Usually sufficient | Rarely needed | Only needed with mutable children |
| Resources, singletons | Dangerous | Usually inappropriate | Forbid cloning via an exception |
As a rule of thumb: as soon as a class has a mutable, nested object property, object cloning without __clone() is a latent bug. Only with purely scalar values or deliberately shared, immutable objects is the default shallow copy actually the correct behavior.
Mironsoft
PHP architecture, object design and maintainable backend systems
Shared state caused by broken object cloning?
We review existing PHP classes for missing or incomplete __clone() implementations and build clean deep copy strategies for nested objects and the Prototype pattern.
Cloning audit
Check classes with object properties for missing __clone()
Deep copy refactoring
Have nested objects and arrays duplicated correctly
Prototype pattern
Replace expensive object creation with cloned templates
10. Summary
Object cloning in PHP is technically simple with clone, but semantically full of pitfalls. The default shallow copy only duplicates the top level of an object, nested objects and the objects contained in arrays remain shared as long as __clone() does not specify otherwise. The __clone() magic method is the central lever for deliberately turning this shallow copy into a deep copy, but it needs to be implemented consistently at every affected level of the object structure.
Readonly properties change the rules since PHP 8.1 in one important detail, from PHP 8.3 onward they may be reassigned inside __clone(). The Prototype pattern uses object cloning as a deliberate creation strategy for families of similar objects. For resources and singletons, object cloning is often the wrong choice and should be explicitly forbidden via an exception in __clone(). Knowing these rules avoids the most common source of silent bugs around shared object state in PHP.
Object Cloning in PHP — Key Takeaways
Default behavior
clone produces a shallow copy. Nested objects and objects in arrays remain shared without __clone().
Deep copy with __clone()
Treat every object property and every object in an array individually with clone, recursively at every level.
Readonly properties
Reassignment inside __clone() is only allowed from PHP 8.3. Before that, a fresh instance is required.
Limits
Resources and singletons should explicitly forbid cloning via an exception in __clone().