# How do I cleanly implement "withX" copy methods on readonly value objects?

> A readonly withX returns a new instance: `new self` with named arguments is portable, and `clone($this, [...])` fits PHP 8.5 when validation is simple.

- Asked: 2026-08-25
- Answered: 2026-08-28
- Asked by: Kaan
- Tags: php, oop
- Source: https://muhammetsafak.com/just-ask/how-do-i-cleanly-implement-withx-copy-methods-on-readonly-value/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** In my domain layer I have value objects like Money and Address, and I made them immutable using PHP 8.2's `readonly` feature. My goal is that once an object is constructed it never changes.

But when I try to produce immutable copies I keep getting "Cannot modify readonly property." For example, in a `withAmount()` method I try to update the property and it blows up. How do I write these "withX" copy methods cleanly and without repeating myself?


Short answer: with `readonly` there's no mutation; instead of trying to change the property when you copy, you must return a new instance.

## Short answer

The error is telling you exactly that — `$this->amount = ...` is forbidden, because a readonly property can only be set once, in the scope where it was initialized. I wrote about what `readonly` buys you in domain models in [the PHP 8.1 post](/blog/php-8-1-safer-domain-models-with-enums-and-readonly/); the withX pattern is the natural consequence of that guarantee.

## Why

1. **Root cause.** After the initial assignment you can't rewrite a readonly property, not even inside `clone` (in an ordinary method); the runtime blocks this on purpose. So mutating `$this` inside withX is impossible — and that's a feature, not a limitation to fight. (Small correction: `readonly` shipped in PHP 8.1, not PHP 8.2.)

2. **PHP 8.3+: `__clone` is for deep-clone only.** In 8.3 you can reinitialize readonly properties inside `__clone`, but that doesn't carry a new value into withX from the outside; its real purpose is deep-copying nested objects (e.g. a `DateTimeImmutable`). Don't try to solve withX with it.

3. **The two paths differ in validation behavior.** Because `new self` runs every copy through the constructor, you get validation (negative amount, invalid currency) for free. `clone($this, [...])` does not call the constructor; if you have invariant checks you have to trigger them by hand on the clone path.

## What to do

1. **Portable, clean pattern: `new self` + named arguments.** Each withX calls the constructor with new values, so all invariants get re-validated. It works everywhere since PHP 8.1:

   ```php
   public function withAmount(int $amount): static
   {
       return new self(amount: $amount, currency: $this->currency);
   }
   ```

2. **Named arguments kill the repetition as fields grow.** In a multi-field VO each withX only writes the field that changes and carries the rest via `$this->...`; named arguments remove the positional coupling and keep it readable.

3. **On PHP 8.5, collapse the wither to one line.** 8.5 lets you override properties while cloning:

   ```php
   public function withAmount(int $amount): static
   {
       return clone($this, ['amount' => $amount]);
   }
   ```

**Bottom line:** personally, on PHP 8.2/8.3 I'd go with `new self` + named arguments — it's portable and runs every copy through validation. If you're on 8.5 and the validation is simple, `clone($this, [...])` is more elegant and cuts the boilerplate; but if your invariants are strict, I'd still prefer the constructor-based path so every copy re-enters the same validation gate and I don't quietly lose that guarantee.

## Related Reading

- [Designing Value Objects in Modern PHP](/blog/designing-value-objects-in-modern-php/) — Blog
- [PHP 8.1 Is Coming: Safer Domain Models with Enums and Readonly Properties](/blog/php-8-1-safer-domain-models-with-enums-and-readonly/) — Blog
- [For concurrent HTTP calls, should I reach for raw Fibers, ReactPHP/AMPHP, or Swoole?](https://muhammetsafak.com/just-ask/concurrent-http-calls-raw-fibers-reactphp-amphp-swoole/) — Just Ask
- [Should I bind a Money value object to Eloquent with a custom cast or with accessors/mutators?](https://muhammetsafak.com/just-ask/should-i-bind-a-money-value-object-to-eloquent-with-a/) — Just Ask
- [Should I use PHPStan generics for type-safe collections or hand-write a class per type?](https://muhammetsafak.com/just-ask/should-i-use-phpstan-generics-for-type-safe-collections-or-hand/) — Just Ask
