In PHP, you clone a draft quote and move the copy's DateTime expiry forward, but the original's expiry changes too; why, and how do you fix it?
answer
- clone copies property values, not graphs
- object property copies a handle
- DateTime::modify() mutates in place
- __clone() runs on the new copy
- DateTimeImmutable returns new instances
basics
~10 sclone is shallow: an object-valued property is copied as a handle, so both quotes share one DateTime and modify() changes it for both. Deep-copy it in __clone() or store a DateTimeImmutable instead.
solid answer
~40 s`clone $quote` creates a new object and copies each property's **value**. For an object-valued property that value is a **handle**, so the original and the copy point at the same `DateTime`. `DateTime::modify()` and `add()` mutate that instance in place, so moving the expiry on the copy moves it on the original too. Two fixes: implement `__clone()` and replace shared mutable parts, `$this->validUntil = clone $this->validUntil;` (it runs on the new object after the shallow copy); or make the property a `DateTimeImmutable`, whose `modify()` returns a new instance and leaves the shared one untouched. `readonly` does not help on its own: it stops the property being reassigned, not the `DateTime` behind it being mutated. Since PHP 8.3, `__clone()` may reinitialise readonly properties once, so the deep-copy fix also works on readonly classes.
code
php · 23 lines<?php
declare(strict_types=1);
final class Quote
{
public function __construct(
public readonly DateTime $validUntil,
public array $lines = [],
) {}
public function __clone(): void
{
// Runs on the copy; readonly may be reinitialised here since PHP 8.3.
$this->validUntil = clone $this->validUntil;
}
}
$draft = new Quote(new DateTime('2026-10-01'));
$revised = clone $draft;
$revised->validUntil->modify('+14 days');
echo $draft->validUntil->format('Y-m-d'), PHP_EOL; // 2026-10-01
echo $revised->validUntil->format('Y-m-d'), PHP_EOL; // 2026-10-15go deeper
Recall that clone copies property values and that an object property's value is a handle, so nested objects stay shared.
Explain when __clone() runs, that $this is the copy, and how to deep-copy object and array-of-object properties.
Diagnose shared-state bugs after cloning, decide per property whether to share, deep-copy or switch to immutable types, and cover readonly limits.
Argue for immutable value types across a codebase versus disciplined __clone() implementations, and what each costs in allocation and review effort.
## What clone copies `clone $object` asks the engine for a new instance of the same class and copies every property's **current value** into it. The manual calls this a **shallow copy**. What "value" means depends on the property's type: | Property holds | What the copy gets | Shared with the original? | |---|---|---| | int, float, string, bool, null | the same scalar | no — scalars are values | | an array of scalars | the array (copy-on-write) | no, once either side writes | | an array of objects | the array of handles | the objects inside are shared | | an object | a copy of the handle | **yes, the same instance** | | a PHP reference (`&`) | the reference | yes — references stay references | No constructor runs for the copy. After the copy is complete, PHP calls the copy's `__clone()` method if the class defines one. ## The draft-quote bug A `Quote` has a `validUntil` property holding a mutable `DateTime`. A salesperson duplicates a quote to offer a longer deadline: 1. `$revised = clone $draft;` — `$revised->validUntil` and `$draft->validUntil` hold handles to one `DateTime`. 2. `$revised->validUntil->modify('+14 days');` — `DateTime::modify()` changes that one object in place. 3. `$draft->validUntil` now shows the new date too, because there is only one date. Nothing warns you. The same happens with line-item objects inside an array, a `Money` object, or any collaborator the quote holds. ## Fix 1 — deep-copy in __clone() `__clone()` is called on the **new** object, so `$this` is the copy. Reassign each property that points to mutable state you must own: - `$this->validUntil = clone $this->validUntil;` - for arrays of objects, map them: `$this->lines = array_map(fn (Line $l) => clone $l, $this->lines);` The cloning recurses only as far as you write it. Services and shared, stateless collaborators (a clock, a tax-rate table) are normally left shared on purpose. `__clone()` must return nothing (declared `: void` if typed), and a `private` or `protected` `__clone()` blocks cloning from outside the class with `Call to private method Quote::__clone() from global scope`. ## Fix 2 — make the shared part immutable `DateTimeImmutable` has the same API shape, but `modify()`, `add()` and `setDate()` return a **new** instance and leave the receiver unchanged. If `validUntil` is a `DateTimeImmutable`, sharing it between clones is harmless, and the revision becomes `$revised->validUntil = $draft->validUntil->modify('+14 days');`. Immutable collaborators remove the need for deep copies altogether, which is why many codebases standardise on `DateTimeImmutable`. ## Readonly does not make a graph immutable `public readonly DateTime $validUntil` forbids **reassigning** the property after initialisation, but the `DateTime` it points to is still mutable, so the clone bug is unchanged. Since **PHP 8.3** a readonly property may be reinitialised inside `__clone()`, once per clone, which lets a readonly `Quote` deep-copy its date. Before 8.3 that assignment threw `Cannot modify readonly property`. PHP 8.5 also offers `clone($draft, ['validUntil' => $newDate])` to replace a property on the copy directly. ## Objects that refuse to be cloned Some internal objects have no clone handler: `clone` on an enum case, a `Generator` or a `PDO` connection throws `Error: Trying to clone an uncloneable object of class ...`. A quote holding such a collaborator can still be cloned; the property simply keeps sharing the handle. ## Proving the copy is independent A shallow-copy bug hides until two copies live side by side, so test it directly: 1. Build a quote with every object-valued property populated. 2. Clone it and mutate each nested object through the copy. 3. Assert that the original's values are unchanged, and that `$copy->validUntil !== $original->validUntil` for every property the copy is meant to own. The same test catches the regression when someone later adds a new mutable property and forgets to extend `__clone()`. ## Checklist - Treat `clone` as shallow, always. - List the object-valued properties and decide for each: share, deep-copy, or make immutable. - Prefer immutable value types (`DateTimeImmutable`, value objects) for data that is copied around. - Test clones by mutating the copy and asserting the original is unchanged.
- Is there a built-in deep clone in PHP?No. `clone` is always shallow and `__clone()` deep-copies only what you write in it. A serialize-and-unserialize round trip is sometimes used as a generic deep copy, but it is slow, runs serialization hooks, and fails for objects that forbid serialization such as closures, so an explicit `__clone()` is the normal answer.
- In `__clone()`, does `$this` refer to the original or to the copy?To the copy. The engine finishes the shallow copy first and then calls `__clone()` on the new object, so assignments in it change only the copy. The original is not passed in; if you need a value from it, it is already in the copied property.
- The quote holds an array of `Line` objects. Is the array copied or shared?The array is a value, so the copy gets its own array once either side adds or removes an element. The `Line` objects inside are handles, though, so editing `$revised->lines[0]->qty` edits the draft's line too. Deep-copy them with `array_map(fn (Line $l) => clone $l, $this->lines)` in `__clone()`.
saying these in an interview costs you the question
- clone copies the whole object graph, including nested objects.
- The constructor runs again for the cloned object.
- In __clone(), $this refers to the original object being copied.
- A readonly DateTime property cannot be mutated through the clone.
- DateTimeImmutable::modify() changes the instance it is called on.