PHP 8.5 clone with: Updating Readonly Objects Without Boilerplate

PHP 8.5 clone with lets you update readonly object properties during cloning — no constructor rebuilds, no wither method boilerplate. Works with Laravel.

Steven Richardson
Steven Richardson
· 4 min read

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).

FAQ#

Can I use clone with from outside the class?

No, readonly visibility rules apply. clone($object, [...]) only works from within the object's own methods. From outside, you'll get an error about modifying readonly properties. If you need external cloning, switch to PHP 8.4's asymmetric visibility (public string $property { get; }) instead of readonly.

Does clone with deep-copy nested objects?

No, clone remains shallow. Nested objects are not cloned — the new instance holds references to the same child objects as the original. Use __clone() to perform deep copying if you need it, or pass new instances explicitly in the array.

What if I need to set multiple properties at once?

Just pass multiple key-value pairs in the array: clone($this, ['city' => 'Manchester', 'postcode' => 'M1 1AE']). All the overrides are applied to the single cloned instance before it returns.

Do I still need wither methods if I use clone with?

Yes, wither methods remain the public API. clone with is just the implementation detail inside them. Wither methods provide type safety and intent, while clone with handles the mechanics without boilerplate.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.