readonly prevents reassignment of the property and says nothing about what the property points at, which is obvious for objects and surprising for arrays.
final class Basket
{
public function __construct(
public readonly array $lines,
public readonly Customer $customer,
) {}
}
$b->lines[] = $line; // Error — arrays are values, so
// this is a modification of $lines
$b->customer->name = 'x'; // FINE. the reference is readonly;
// the object it points at is not.
The array case works better than expected because PHP arrays are value types, so appending is a write to the property and is refused. The object case is the real gap: a readonly property holding a mutable object gives an immutable reference to a mutable thing, which is a guarantee nobody wants. The only fix is that the contained objects are themselves immutable, all the way down, and nothing in the language checks that — it is a discipline with a keyword that looks like enforcement.