> On 11. Jul 2025, at 01:43, Rob Landers <rob@bottled.codes> wrote: > > On Thu, Jul 10, 2025, at 17:34, Larry Garfield wrote: >> On Thu, Jul 10, 2025, at 5:43 AM, Tim Düsterhus wrote: >> > Hi >> > >> > Am 2025-07-08 17:32, schrieb Nicolas Grekas: >> >> I also read Tim's argument that new features could be stricter. If one >> >> wants to be stricter and forbid extra behaviors that could be added by >> >> either the proposed hooks or __get, then the answer is : make the class >> >> final. This is the only real way to enforce readonly-ness in PHP. >> > >> > Making the class final still would not allow to optimize based on the >> > fact that the identity of a value stored in a readonly property will not >> > change after successfully reading from the property once. Whether or not >> > a property hooked must be considered an implementation detail, since a >> > main point of the property hooks RFC was that hooks can be added and >> > removed without breaking compatibility for the user of the API. >> > >> >> 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. >> > >> > That is saying "it's impossible to introduce bugs". >> > >> >> PS: as I keep repeating, readonly doesn't immutable at all. I know this >> >> is >> >> written as such in the original RFC, but the concrete definition and >> >> implementation of readonly isn't: you can set mutable objects to >> >> readonly >> >> properties, and that means even readonly classes/properties are >> >> mutable, in >> >> the generic case. >> > >> > `readonly` guarantees the immutability of identity. While you can >> > certainly mutate mutable objects, the identity of the stored object >> > doesn't change. >> > >> > Best regards >> > Tim Düsterhus >> >> Nick previously suggested having the get-hook's first return value cached; >> it would still be subsequently called, so any side effects would still >> happen (though I don't know why you'd want side effects), but only the first >> returned value would ever get returned. Would anyone find that acceptable? >> (In the typical case, it would be the same as the current $this->foo ??= >> compute() pattern, just with an extra cache entry.) >> >> --Larry Garfield >> > > I think that only covers one use-case for getters on readonly classes. Take > this example for discussion: > > readonly class User { > public int $elapsedTimeSinceCreation { get => time() - $this->createdAt; } > private int $cachedResult; > public int $totalBalance { get => $this->cachedResult ??= 5+10; } > public int $accessLevel { get => getCurrentAccessLevel(); } > public function __construct(public int $createdAt) {} > } > > $user = new User(time() - 5); > var_dump($user->elapsedTimeSinceCreation); // 5 > var_dump($user->totalBalance); // 15 > var_dump($user->accessLevel); // 42 > > In this example, we have three of the most common ones: > Computed Properties (elapsedTimeSinceCreation): these are properties of the > object that are relevant to the object in question, but are not static. In > this case, you are not writing to the object. It is still "readonly". > Memoization (expensiveCalculation): only calculate the property once and only > once. This is a performance optimization. It is still "readonly". > External State (accessLevel): properties of the object that rely on some > external state, which due to architecture or other convienence may not make > sense as part of object construction. It is still "readonly". > You can mix-and-match these to provide your own level of immutability, but > memoization is certainly not the only one. > > You could make the argument that these should be functions, but I'd posit > that these are properties of the user object. In other words, a function to > get these values would probably be named `getElapsedTimeSinceCreation()`, > `getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway. > > — Rob
Hey Rob, We ended up where we are now because more people than not voiced that they would expect a `readonly` property value to never change after `get` was first called. As you can see in my earlier mails I also was of a different opinion. I asked "what if a user wants exactly that”? You brought good examples for when “that" could be the case. It is correct, with the current alternative implementations your examples would be cached. A later call to the property would *not* use the updated time or a potentially updated external state. After thinking a lot about it over the last days I think that makes sense. To stick to your usage of `time()`, I think the following is a good example: ```php readonly class JobHelper { public function __construct( public readonly string $uniqueRunnerKey { get => 'runner/' . date("Ymd_H-i-s", time()) . '_' . (string) random_int(1, 100) . '/'. $this->uniqueRunnerKey; } ) {} } $helper = new JobHelper('report.txt’); $key1 = $helper->uniqueRunnerKey; sleep(2); $key2 = $helper->uniqueRunnerKey; var_dump($key1 === $key2); // true ``` It has two dynamic path elements, to achieve some kind of randomness. As a user you still can expect $key1 === $key2 to hold when using `readonly`. Claude's argument is strong, because we also cannot write twice to a `readonly` property. So it’s fair to say reading should also be predictable, and return the exact same value on consecutive calls. If users don’t want that, they can opt-out by not using `readonly`. The guarantee only holds in combination with `readonly`. Alternatively, as you proposed, using methods (which I think would really be a better fit; alternatively virtual properties which also will not support `readonly`. With what we have now, both “camps" will be able to achieve what they want transparently. And I believe that’s a good middle ground we should go forward with. Cheers, Nick