readonly class (PHP 8.2) makes every property readonly and forbids dynamic ones. PHP 8.3 lets __clone() reinitialize readonly properties, so a clone can take its own copy of a mutable object, and PHP 8.5's clone($obj, [...]) (Subsection 4.9.11) copies and changes properties in one step.
<?php
enum OrderStatus: string { case Paid = 'paid'; case Shipped = 'shipped'; } // trimmed
final readonly class Order {
public function __construct(
public int $id, public OrderStatus $status, public DateTime $at) {}
public function __clone() { $this->at = clone $this->at; } // 8.3: deep copy allowed
public function withStatus(OrderStatus $s): static {
return clone($this, ['status' => $s]); // 8.5: clone with
}
}
$a = new Order(7, OrderStatus::Paid, new DateTime('2026-07-14'));
$b = $a->withStatus(OrderStatus::Shipped);
$b->at->modify('+2 days'); // touches only $b's copy
printf("%s %s | %s %s\n", $a->status->name, $a->at->format('m-d'),
$b->status->name, $b->at->format('m-d'));
try { clone($a, ['id' => 8]); } catch (Error $e) { echo $e->getMessage(), "\n"; }Output
Paid 07-14 | Shipped 07-16 Cannot modify protected(set) readonly property Order::$id from global scope
__clone() runs before the new values are assigned, and they are assigned with the caller's visibility: inside withStatus() the write succeeds, while from outside it meets the implicit protected(set).