> On 11. Jul 2025, at 01:43, Rob Landers <[email protected]> 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