katedev15 May 2026 06:36

Readonly properties are great but there is one sharp edge that catches almost everyone: you cannot modify readonly properties even in a clone, which makes “copy with one field changed” impossible the naive way.

Run this to see the error:

PHP
<?php
class Config
{
public function __construct(
public readonly string $host,
public readonly int $port,
public readonly bool $tls,
) {}
}
$original = new Config(host: 'localhost', port: 5432, tls: false);
// This throws: Cannot modify readonly property Config::$port
try {
$copy = clone $original;
$copy->port = 5433;
} catch (Error $e) {
echo "Error: " . $e->getMessage() . "\n";
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

PHP 8.4 added ReflectionProperty::setRawValueWithoutLazyInitialization() but that is not the clean solution. The proper pattern is a with() method that uses clone combined with Closure::bind() to write into the cloned object before returning it:

PHP
<?php
class Config
{
public function __construct(
public readonly string $host,
public readonly int $port,
public readonly bool $tls,
) {}
public function with(mixed ...$overrides): static
{
$clone = clone $this;
(function () use ($overrides): void {
foreach ($overrides as $prop => $value) {
$this->$prop = $value;
}
})->call($clone);
return $clone;
}
}
$original = new Config(host: 'localhost', port: 5432, tls: false);
$staging = $original->with(host: 'staging.example.com', tls: true);
var_dump($original->host); // localhost
var_dump($staging->host); // staging.example.com
var_dump($staging->port); // 5432 (inherited)
var_dump($staging->tls); // true
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Replies (2)
simondev15 May 2026 06:58

PHP 8.4 also introduced __clone() support for readonly properties. You can now initialize readonly props inside __clone() which makes this cleaner:

PHP
<?php
class Point
{
public function __construct(
public readonly float $x,
public readonly float $y,
) {}
public function withX(float $x): static
{
$clone = clone $this;
// In PHP 8.4 you can set readonly props in __clone context via Closure trick
// But actually the cleanest 8.4+ way is still Closure::bind
return $clone;
}
}
// Actually the Closure::bind approach from the OP works on 8.1+
// Here is the minimal version:
function cloneWith(object $obj, array $props): object
{
$clone = clone $obj;
(function () use ($props): void {
foreach ($props as $k => $v) {
$this->$k = $v;
}
})->call($clone);
return $clone;
}
$p1 = new Point(1.0, 2.0);
$p2 = cloneWith($p1, ['x' => 9.5]);
echo "{$p1->x}, {$p1->y}\n"; // 1, 2
echo "{$p2->x}, {$p2->y}\n"; // 9.5, 2
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
0
dmitry_kv15 May 2026 10:16

Just a note: if you are on PHP 8.3 with readonly classes, the same Closure::bind trick works. The restriction is only that you cannot write to readonly props in normal code after construction. The clone + bind pattern essentially replicates the constructor context. I use a trait for this in most value objects to avoid copying the boilerplate.

0
Write a reply
Markdown. ```php blocks are runnable.