Re: [RFC] Readonly property hooks
| From: | Rob Landers | Date: | Fri, 11 Jul 2025 11:57:47 +0000 |
| Subject: | Re: [RFC] Readonly property hooks | ||
| References: | 1 2 3 4 5 6 7 8 9 10 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-128011@lists.php.net to get a copy of this message | ||
On Fri, Jul 11, 2025, at 13:40, Nick wrote:
>
>> 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:
>> 1. 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".
>> 2. Memoization (expensiveCalculation): only calculate the property once and only once.
>> This is a performance optimization. It is still "readonly".
>> 3. 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
Hey Nick,
After sleeping on it, I think I agree with this assessment. For backed properties, especially, it
makes sense that the value feels "immutable". If and when we get to virtual properties,
maybe not. But that's a bridge to cross later.
— Rob