On Wed, Jul 9, 2025, at 5:52 AM, Claude Pache wrote: >> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+...@gmail.com> a >> écrit : >> >> I read Claude's concern, and I agree with Larry's response: the engine >> already allows readonly to be bypassed using __get. The added hook doesn't >> make anything more lenient. >> > > It is true that readonly could be bypassed by __get(); but this is a > legacy behaviour, and you have to take an explicit step to make it > possible. For those unaware of the awful hack, here is a minimal test > case: > > https://3v4l.org/N78An > > where the `unset(...)` is mandatory to make it “work”. > > Are we obligated to sanction shortcomings of legacy concepts in newly > introduced concepts that are supposed to replace them? Or can we do > something better? I’ve outlined in a previous email what I think is a > better design for such situation (namely an `init` hook). > > Also, the fact that __get() is not yet deprecated means that we can > still use the aforementioned hack until/unless we’ve implemented a > proper solution. In the worst case, you can still use a non-readonly > hooked property and document the intended invariants in phpdoc. > > >> If a class is final and uses readonly with either hooks or __get, then >> that's the original author's choice. There's no need for extra >> engine-assisted strictness in this case. You cannot write such code in a >> non-readonly way by mistake, so it has to be by intent. >> > > Enforcing as strictly as possible its intended invariants is a good > design for a robust language. Yes, it implies that users cannot (or can > hardly) escape annoying constraints. For example, you can’t extend a > final class, even if you think that you have good reason for it, like > constructing a mock object. > > —Claude
Here's the core problem right now: 1. `readonly` bills itself as immutability, but it fundamentally is not. There are at least two loopholes: __get and a mutable object saved to a property. So while it offering immutability guarantees is nice in theory, it's simply not true in practice. `readonly` has always been misnamed; it should really be `writeonce`, because that's all it is. (Once again, this is likely the most poorly designed feature we've added in many years.) 2. In 8.4, if a class is marked `readonly`, you basically forbid it from having any hooks of any kind, even though you absolutely can honor the write-once-ness of the properties while still having hooks. And that applies to child classes, too, because `readonly`-ness inherits. So adding a single hook means you have to move the readonly to all the other properties individually, which if inheritance is involved you cannot do. The RFC aims to address point 2 in a way that still respects point 1, but only point 1 as it actually is (write-once), not as we wish it to be (immutability). In practice, there's 2 scenarios that I see as useful (or problematic in 8.4, that we want to support): * set hooks for validation, which don't impact writeonce-ness. I think everyone seems on board with that. * Lazy computed properties. I use these a ton, even for internal caching purposes. 99% of the time I cache them because my objects are practically immutable, and $this->foo ??= whatever is an easy enough pattern. (If they're not cached then it would be a virtual property, which we're not dealing with for now.) As long as you're caching it in that fashion, the write-once-ness still ends up respected. Honestly, Nick tried to come up with examples yesterday while we were talking that would not fit into one of those two categories, and for every one of them my answer was "if your code is already that badly designed, there's nothing we can do for you." :-) Ilija and I had discussed making `readonly` imply cached/lazy/init in the original hooks RFC, but decided against it. Mainly, it becomes very confusing if a property is going to store a value, as there's three different scenarios to consider: There's a short-set hook, the property is mentioned in its own hooks, and then look for readonly. (Would that mean readonly only works on virtual properties?) It makes a feature that's already, in all honesty, at the edge of reasonable complexity more complex. An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???" I'd be open to a follow up RFC for an init hook, though I likely wouldn't write it myself. But that's a different topic than what we're addressing here. --Larry Garfield