Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128010 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 56E161A00BC for ; Fri, 11 Jul 2025 11:41:19 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752233969; bh=PEhP0uiu4lVBbZv7LjbLpdk6Y3z08Dfa+XNianpW3QQ=; h=From:Subject:Date:In-Reply-To:Cc:To:References:From; b=X43TzLRr8QDX9/kz+prX/zhmZwapBuXs4HvovIHOhaPi+diZ4GWT07gdGZyPONpEV HzaYMZhzPR4aoBjXCkk7BkOwAHGDrK732NvNIdcH/7bqE/bEiKoR3NQKhKM1F88LvB +hxRhyD6evfbMj/I7s50OWOtDnbBBPqdvJy1sdBKfbUSBgT7wlodFH8NjGKIIIvxAi g4xKhYf2bWVPkoxiKxRSGYLmWRPXfsUuqngt3qyMwnomU7JGMjYrIBId+BnEzYMLY4 kZSm0jm+ZzFHlqoGfSvnYJ0KYeu5mGZ3zFIdHiGoXniEYWThHbunpoYrRb0/YVLB1a aqO5NiH2z2rPQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id D6427181003 for ; Fri, 11 Jul 2025 11:39:27 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.7 required=5.0 tests=BAYES_05,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,HTML_MESSAGE, SPF_HELO_NONE,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from avril.gn2.hosting (avril.gn2.hosting [84.19.162.247]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Fri, 11 Jul 2025 11:39:27 +0000 (UTC) Received: from avril.gn2.hosting (localhost [127.0.0.1]) by avril.gn2.hosting (Postfix) with ESMTP id B06F81C4082C; Fri, 11 Jul 2025 13:41:13 +0200 (CEST) Received: from smtpclient.apple (unknown [111.68.29.103]) by avril.gn2.hosting (Postfix) with ESMTPSA id D2BBB1C405F1; Fri, 11 Jul 2025 13:41:08 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=nicksdot.dev; s=default; t=1752234069; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: in-reply-to:in-reply-to:references:references; bh=P9rVZneMWqM4pslq9VAMIncv4JpOr6pOVnTCbayPMhs=; b=YAh2x1tOouq1gQ+g+CuPbV/MC5VhSwrz/aNKc1DM820kf9+Vj65Ds94q9UnGH3l8J89o74 FI9NOShFnbSuf0XWWkDu5R52ku7uaxPQxe7Hh9tgRttmu+4ah47iGlChBBXHPf/qEkggkd NNxudjy3e3cBP5iqN+xt3q58Z3U3LaLQX8/7Hdm1mqr+Mzo5b0P6O7t/OZI3badRSyATqv +ChCTJOxAvv7un3Fty5JOGJVYX4bTk9aDdZl0N4753CBxDW+wvcMJARBbuXgnyBEi9ZGA6 ai5F4VlsFvCpqqT4URiP7QS1rp/sLZSsejzjynso9WZ2jxX6ohhMDdB9931tTQ== Message-ID: Content-Type: multipart/alternative; boundary="Apple-Mail=_16E8073C-BC1A-4687-AA3E-0C7B3902E530" Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3826.600.51.1.1\)) Subject: Re: [PHP-DEV] [RFC] Readonly property hooks Date: Fri, 11 Jul 2025 18:40:46 +0700 In-Reply-To: Cc: internals@lists.php.net To: Rob Landers References: <1e8634d7-ac1a-4025-b4e2-1948aabf5251@app.fastmail.com> <6acab95a554fe5e188364840ea36d2b7@bastelstu.be> X-Mailer: Apple Mail (2.3826.600.51.1.1) From: php@nicksdot.dev (Nick) --Apple-Mail=_16E8073C-BC1A-4687-AA3E-0C7B3902E530 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 > On 11. Jul 2025, at 01:43, Rob Landers wrote: >=20 > On Thu, Jul 10, 2025, at 17:34, Larry Garfield wrote: >> On Thu, Jul 10, 2025, at 5:43 AM, Tim D=C3=BCsterhus 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=20 >> > fact that the identity of a value stored in a readonly property = will not=20 >> > change after successfully reading from the property once. Whether = or not=20 >> > a property hooked must be considered an implementation detail, = since a=20 >> > main point of the property hooks RFC was that hooks can be added = and=20 >> > removed without breaking compatibility for the user of the API. >> > >> >> engine-assisted strictness in this case. You cannot write such = code in=20 >> >> 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=20 >> >> is >> >> written as such in the original RFC, but the concrete definition = and >> >> implementation of readonly isn't: you can set mutable objects to=20= >> >> readonly >> >> properties, and that means even readonly classes/properties are=20 >> >> mutable, in >> >> the generic case. >> > >> > `readonly` guarantees the immutability of identity. While you can=20= >> > certainly mutate mutable objects, the identity of the stored object=20= >> > doesn't change. >> > >> > Best regards >> > Tim D=C3=BCsterhus >>=20 >> 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 ??=3D compute() pattern, just with an extra cache = entry.) >>=20 >> --Larry Garfield >>=20 >=20 > I think that only covers one use-case for getters on readonly classes. = Take this example for discussion: >=20 > readonly class User { > public int $elapsedTimeSinceCreation { get =3D> time() - = $this->createdAt; } > private int $cachedResult; > public int $totalBalance { get =3D> $this->cachedResult ??=3D = 5+10; } > public int $accessLevel { get =3D> getCurrentAccessLevel(); } > public function __construct(public int $createdAt) {} > } >=20 > $user =3D new User(time() - 5); > var_dump($user->elapsedTimeSinceCreation); // 5 > var_dump($user->totalBalance); // 15 > var_dump($user->accessLevel); // 42 >=20 > 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.=20 >=20 > 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. >=20 > =E2=80=94 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=E2=80=9D?=20 You brought good examples for when =E2=80=9Cthat" could be the case. =20 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.=20 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 =3D> 'runner/' . date("Ymd_H-i-s", time()) . '_' . = (string) random_int(1, 100) . '/'. $this->uniqueRunnerKey; } ) {} } $helper =3D new JobHelper('report.txt=E2=80=99); $key1 =3D $helper->uniqueRunnerKey; sleep(2); $key2 =3D $helper->uniqueRunnerKey; var_dump($key1 =3D=3D=3D $key2); // true ``` It has two dynamic path elements, to achieve some kind of randomness. As = a user you still can expect $key1 =3D=3D=3D $key2 to hold when using = `readonly`. Claude's argument is strong, because we also cannot write twice to a = `readonly` property. So it=E2=80=99s fair to say reading should also be predictable, and = return the exact same value on consecutive calls. If users don=E2=80=99t 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 =E2=80=9Ccamps" will be able to achieve what = they want transparently. And I believe that=E2=80=99s a good middle ground we should go forward = with. Cheers, Nick= --Apple-Mail=_16E8073C-BC1A-4687-AA3E-0C7B3902E530 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8
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=C3=BCsterhus = 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=C3=BCsterhus

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 ??=3D = 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 =3D> time() - $this->createdAt; }
    = private int $cachedResult;
    public int = $totalBalance { get =3D> $this->cachedResult ??=3D 5+10; = }
    public int $accessLevel { get =3D> = getCurrentAccessLevel(); }
    public function = __construct(public int $createdAt) = {}
}

$user =3D 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.

=E2=80=94 = 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=E2=80=9D? 
You brought good = examples for when =E2=80=9Cthat" 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 =3D> 'runner/' . date("Ymd_H-i-s", time()) . '_' . (string) = random_int(1, 100) . '/'. $this->uniqueRunnerKey;
}
= ) {}
}

$helper =3D = new = JobHelper('report.txt=E2=80=99);
$key1 =3D $helper->uniqueRunnerKey;
sleep(2);
$key2 =3D = $helper->uniqueRunnerKey;
var_dump($key1 =3D=3D=3D $key2); // = true
```

It has = two dynamic path elements, to achieve some kind of randomness. As a user = you still can expect $key1 =3D=3D=3D $key2 to hold when using = `readonly`.

Claude's argument is strong, = because we also cannot write twice to a `readonly` = property.
So it=E2=80=99s fair to say reading should also be = predictable, and return the exact same value on consecutive = calls.

If users don=E2=80=99t 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 = =E2=80=9Ccamps" will be able to achieve what they want = transparently.
And I believe that=E2=80=99s a good middle = ground we should go forward = with.

Cheers,
Nick
<= /html>= --Apple-Mail=_16E8073C-BC1A-4687-AA3E-0C7B3902E530--