Hello Larry,
> Regarding your main question: I understand your problem with readonly > > classes, and I'd be happy if we found a solution which fits your > use-cases > > and keeps consistency for the engine at the same time. To give you more > > context about the inheritance related restriction in the RFC: I went with > > forbidding the extension of readonly classes by non-readonly ones so that > > the invariants of the parent (no dynamic or mutable properties are > allowed) > > don't occasionally get violated by the child. However, I cannot think > about > > a proper scenario now where this would cause any problems... I'm > wondering > > if anyone could come up with one? > > I don't have a real-world example, but a general pattern. The advantage > of a readonly object (either conventionally like PSR-7 or enforced by the > `readonly` keyword) is that you can store a reference to it without having > to worry about it changing out from under you. That means you can code on > the assumption that any data derived from that object is also not subject > to change. > > If you accept a readonly object of type Foo, you would write code around > it on that assumption. > > If you're then passed an object of type Bar extends Foo, which has an > extra non-readonly property, that assumption would now be broken. Whether > that counts as a Liskov violation is... a bit squishy, I think. > > Where that might be a problem is a case like this: > > readonly class Person { > public function __construct(public string $first, public string $last) {} > > public function fullName() { > return "$this->first $this->last"; > } > } > > class FancyPerson extends Person { > public string $middle = ''; > > public function setMiddle(string $mid) { > $this->middle = $mid; > } > > public function fullName() { > return "$this->first $this-middle $this->last"; > } > } > > class Display { > public function __construct(protected Person $person) {} > > public function hello() { > return "Hello " . $this->person->fullName(); > } > } > > Now, Display assumes Person is readonly, and thus fullName() is > idempotent, and thus Display::hello() is idempotent. But if you pass a > FancyPerson to Display, then fullName() is not safely idempotent (it may > change out from under you) and thus hello() is no longer idempotent. > > Whether or not that problem actually exists in practice is another > question. And to be fair, the same risk exists for conventionally-readonly > classes today (PSR-7, PSR-13, etc.), unless they're made final. Arguably > the safety signaling of a lexically readonly class is stronger than for a > conventionally readonly class, so one would expect that to not be broken. > But that's the case where a non-readonly child of a readonly parent can > cause problems. > Thanks for constructing this example, that's good food for thoughts. Unfortunately, The code following readonly child class shows that this safety you describe doesn't exist: readonly class FancyPerson extends Person { private readonly stdClass $middle; public function setMiddle(string $mid) { $this->middle ??= new stdClass; $this->middle->name = $mid; } public function fullName() { return "$this->first $this-middle $this->last"; } } > Personally I'm undecided at the moment whether or not it should be > allowed. I'm sympathetic to the "it's easier to disallow and allow later > than vice versa" argument, but still not sure where I stand. The above at > least gives a concrete example of where the problem would be. > If we want the safety you describe, we might want a stronger version of it. Let's name it immutable. An immutable property/class would be like a readonly property with the additional restriction that only an immutable value could be assigned to it (scalars + array + immutable classes.) But that's another topic. Your example made me doubt for a moment, but without any convincing purpose, this should help decide that child classes shouldn't have to be readonly. Nicolas