PHP 8.5 clone with: Updating Readonly Objects Without Boilerplate
Readonly classes are great right up until you need to change one field. The moment you adopt them for value objects and DTOs, you're on the hook for a withCity(), withPostcode(), withName() method per property — each one reconstructing the entire object just to change a single value. PHP 8.5's clone with syntax collapses all of that into a single expression.
The Old Way: Wither Methods
A typical readonly address DTO before PHP 8.5:
final readonly class Address
{
public function __construct(
public string $street,
public string $city,
public string $postcode,
public string $country,
) {}
public function withCity(string $city): static
{
// Reconstruct the full object just to change one field
return new static($this->street, $city, $this->postcode, $this->country);
}
public function withPostcode(string $postcode): static
{
return new static($this->street, $this->city, $postcode, $this->country);
}
}
Every new property means a new method. Every method has to know about all the others. Add a field, update every wither. It's boring, fragile work.
PHP 8.5 clone with Syntax
PHP 8.5 makes clone behave like a function that accepts an array of property overrides:
clone($object, ['property' => $newValue])
That's it. Pass the object, pass an associative array of the properties you want to change, get back a fresh copy with those values applied.
The same Address DTO now needs wither methods that look like this:
final readonly class Address
{
public function __construct(
public string $street,
public string $city,
public string $postcode,
public string $country,
) {}
public function withCity(string $city): static
{
return clone($this, ['city' => $city]);
}
public function withPostcode(string $postcode): static
{
return clone($this, ['postcode' => $postcode]);
}
}
You can update multiple properties at once too:
$updated = clone($address, [
'city' => 'Manchester',
'postcode' => 'M1 1AE',
]);
No more listing every constructor argument. No more "I added a field and forgot to update three wither methods."
A Real-World Example
Here's a Money value object — a classic use case for readonly immutability:
final readonly class Money
{
public function __construct(
public int $amount, // stored in pence/cents
public string $currency,
) {}
public function withAmount(int $amount): static
{
return clone($this, ['amount' => $amount]);
}
public function add(Money $other): static
{
// Guards omitted for brevity
return clone($this, ['amount' => $this->amount + $other->amount]);
}
}
$price = new Money(1999, 'GBP'); // £19.99
$vat = new Money(400, 'GBP'); // £4.00
$total = $price->add($vat); // £23.99 — original unchanged
The original $price is never mutated. clone produces a new instance, applies the overrides from the array, and hands it back.
Gotchas and Edge Cases
Readonly properties can only be updated from inside the class.
This is the big one. The visibility rules PHP normally applies to property writes are enforced by clone(). If $address->city is readonly, this fails:
// This throws an Error — you cannot modify readonly properties from outside the class
$updated = clone($address, ['city' => 'Manchester']);
It only works if you're calling clone($this, [...]) from inside the object's own methods — which is what the wither method pattern above does. If you need to clone with overrides from outside the class, use PHP 8.4's asymmetric visibility (public string $city { get; }) instead of readonly, which allows external writes.
__clone() runs before the array overrides are applied.
If your class implements __clone() to do custom setup (like deep-copying a nested object), that logic fires first — then the property array is applied on top. Keep that order in mind if your __clone() sets values you're also overriding.
final readonly class Order
{
public function __clone(): void
{
// This runs first, THEN the 'status' override from the array is applied
}
}
Clone is still shallow.
Nested objects are not deep-copied — the cloned object holds the same instance references as the original:
final readonly class Order
{
public function __construct(
public Address $shippingAddress, // ← still same Address instance after clone
public string $status,
) {}
}
$shipped = clone($order, ['status' => 'shipped']);
// $shipped->shippingAddress === $order->shippingAddress (same object)
If you need a deep copy of a nested object, do it inside __clone() or pass a new instance explicitly in the array.
Wrapping Up: When to Reach for PHP 8.5 clone with
PHP 8.5 clone with doesn't eliminate wither methods — you still need them to stay inside the class scope — but it cuts each one down to a single line. Pair it with PHP 8.4 property hooks and readonly classes for a clean value-object pattern with minimal ceremony. Requires PHP 8.5 (released November 2025).
Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.