# 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://www.muhammetsafak.com.tr/en/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. 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.

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. Lean into producing a fresh object rather than trying to defeat the restriction. (Small correction: `readonly` shipped in PHP 8.1, not PHP 8.2.)
2. **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);
   }
   ```
3. **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.
4. **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.
5. **PHP 8.5: this is exactly what the wither needs.** 8.5 lets you override properties while cloning, so the wither pattern collapses to one line:
   ```php
   public function withAmount(int $amount): static
   {
       return clone($this, ['amount' => $amount]);
   }
   ```
6. **Don't forget validation.** 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.

**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](/en/blog/designing-value-objects-in-modern-php/) — Blog
- [PHP 8.1 Is Coming: Safer Domain Models with Enums and Readonly Properties](/en/blog/php-8-1-safer-domain-models-with-enums-and-readonly/) — Blog
- [Should I bind a Money value object to Eloquent with a custom cast or with accessors/mutators?](https://www.muhammetsafak.com.tr/en/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://www.muhammetsafak.com.tr/en/just-ask/should-i-use-phpstan-generics-for-type-safe-collections-or-hand/) — Just Ask
- [What do I gain and lose by moving my status class constants to backed enums?](https://www.muhammetsafak.com.tr/en/just-ask/what-do-i-gain-and-lose-by-moving-my-status-class/) — Just Ask
