Serialising an object with readonly properties

unserialize creates an object without calling the constructor and then writes the properties, which is precisely the thing readonly forbids.

final class Money
{
    public function __construct(
        public readonly int $cents,
    ) {}
}

unserialize(serialize(new Money(4900)));
// works — the engine has a documented exception for
// unserialize writing readonly properties once.

$c = clone $m;   // fine
$c->cents = 1;   // Error — clone does not reset the lock

Native serialisation is allowed by an explicit carve-out in the engine, and hydration libraries that use reflection to write properties mostly work for the same reason — ReflectionProperty::setValue can initialise an uninitialised readonly property. The case that genuinely does not work is a wither: clone copies the initialised state, so modifying the copy throws, and 8.1 has no clone with to fix it. The workaround is a new instance from the constructor, which is more verbose and correct.