Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128011 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 A572C1A00BC for ; Fri, 11 Jul 2025 11:58:54 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752235024; bh=jgSYuWWM16Kuei48BVQalmMxZQPFK2FnZy0Mcc1uOps=; h=Date:From:To:Cc:In-Reply-To:References:Subject:From; b=CDlPKSsBs7wRajhPJGDFKBJjJMA7Fcum4ETCnbp0wXKmzCh12cSUf+zrEld8Jzr6G zVDicAZmJ54QCA7V4IlVVK4ka8ana61dV6XMcutulSsMCP1MYsKFH9t7lcUyqHx+5k lRecjst/Be6g/y/rVYKacLk9LnaIX1wrIguVjGP9phLRgClhJ5ohllx4upkbbntbeh +KsySRjVmBtyAmXj3LDe0mhbLjkk4hm/Rnw1FPH2jjUSVFJFPClGMKlc9QA81A/GaU nVD1AzxCmphZD3VU9JPBy3srO5XLpLGGI8xcB3G3gOUqOgYfNd+ozjCHYn+qaTPl9s aLjPHulFaRLmA== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id D4839180FA7 for ; Fri, 11 Jul 2025 11:57:02 +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=-2.8 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,HTML_MESSAGE, RCVD_IN_DNSWL_LOW,SPF_HELO_PASS,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 fout-a5-smtp.messagingengine.com (fout-a5-smtp.messagingengine.com [103.168.172.148]) (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:57:02 +0000 (UTC) Received: from phl-compute-05.internal (phl-compute-05.phl.internal [10.202.2.45]) by mailfout.phl.internal (Postfix) with ESMTP id 850DDEC0136; Fri, 11 Jul 2025 07:58:51 -0400 (EDT) Received: from phl-imap-05 ([10.202.2.95]) by phl-compute-05.internal (MEProxy); Fri, 11 Jul 2025 07:58:51 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bottled.codes; h=cc:cc:content-type:content-type:date:date:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to; s=fm3; t=1752235131; x= 1752321531; bh=FWr/JfxvxB7WC6u9tEcndshHlqq4aF8nLTC4On8zlGw=; b=H zTFm51YYs/n+AwNXd/TAQrikiWK0Ft9KPGwWMWzg4Dwl5+DdJUM9eCiuLoFEG0AD mrmYtoly4abHfxD6aCvntjj+pFS/HuBrFaA2qFmNaKo35rGQJMEivCrDi9ovH3Y2 Sj+JtqG6XU6IPs3WOh27l5adEBzB4H7SQIu3kf/YXJxfZnUzGZUpF9//eBz6+yT9 8TtB/XXVPZUX/6DOktxTJgVycz+JttAC7z5H+r5H3Il4D1Xr6S/PlQ3VWPe3cHt2 RDkrG2FH/EhS+OH+YwY5iTgHBUoKhTqHEVpYRDH6nqOO63CewFzv3EV0zSm/h6MJ Ru9nvr+w7Q0sdXz/FttlA== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm2; t= 1752235131; x=1752321531; bh=FWr/JfxvxB7WC6u9tEcndshHlqq4aF8nLTC 4On8zlGw=; b=k1NAxWoVkBj+NIeZwhESdA5zaVnXwWWwma+2v6oxhQ58+PSRVLW 2QigG48pBt2qq9UOUbVEtxj39zjIYjo7T4DNRG2r/+abVms9qFb7jCH8k6qPXo9A fIFx164jMN3GIzIzGlaQDx6xCkkmDx+pgl1mFffZ2r+jXitmr57CrUYICGBeisBX 0TVSMuwJefa0ggd5JuHBAAma5s6trJhOGpS9/O/UtVsAelMJ1BHX0lPcfPfPFqNH S9S86RnhKsSfYCufEVEBn9/8gpM5EGQ6GosZVV/0gagO/GCS2is2omxArjvrn9X7 dvOdmRzaUF+oe4dDlobuVIolKgG8QVbirbw== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdefgdegfedviecutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpuffrtefokffrpgfnqfghnecuuegr ihhlohhuthemuceftddtnecunecujfgurhepofggfffhvfevkfgjfhfutgesrgdtreerre dtjeenucfhrhhomhepfdftohgsucfnrghnuggvrhhsfdcuoehrohgssegsohhtthhlvggu rdgtohguvghsqeenucggtffrrghtthgvrhhnpeeiueethedvvdefjefhgfeiheelheehtd fhfeekjefflefgvedvkeduteejjedttdenucevlhhushhtvghrufhiiigvpedtnecurfgr rhgrmhepmhgrihhlfhhrohhmpehrohgssegsohhtthhlvggurdgtohguvghspdhnsggprh gtphhtthhopedvpdhmohguvgepshhmthhpohhuthdprhgtphhtthhopehinhhtvghrnhgr lhhssehlihhsthhsrdhphhhprdhnvghtpdhrtghpthhtohepphhhphesnhhitghkshguoh htrdguvghv X-ME-Proxy: Feedback-ID: ifab94697:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id B70F91820074; Fri, 11 Jul 2025 07:58:50 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 X-ThreadId: T5f4527d1a0d4de24 Date: Fri, 11 Jul 2025 13:57:47 +0200 To: Nick Cc: internals@lists.php.net Message-ID: <8ac21ca5-4a7b-47f1-aa75-5ce5bb0301d9@app.fastmail.com> In-Reply-To: References: <1e8634d7-ac1a-4025-b4e2-1948aabf5251@app.fastmail.com> <6acab95a554fe5e188364840ea36d2b7@bastelstu.be> Subject: Re: [PHP-DEV] [RFC] Readonly property hooks Content-Type: multipart/alternative; boundary=3209718a4c3c4143abf2ff13c73e62b6 From: rob@bottled.codes ("Rob Landers") --3209718a4c3c4143abf2ff13c73e62b6 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable On Fri, Jul 11, 2025, at 13:40, Nick wrote: >=20 >> 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. I= f one >>> >> wants to be stricter and forbid extra behaviors that could be add= ed 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 wi= ll not=20 >>> > change after successfully reading from the property once. Whether = or not=20 >>> > a property hooked must be considered an implementation detail, sin= ce a=20 >>> > main point of the property hooks RFC was that hooks can be added a= nd=20 >>> > removed without breaking compatibility for the user of the API. >>> > >>> >> engine-assisted strictness in this case. You cannot write such co= de 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 kno= w 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 objec= t=20 >>> > doesn't change. >>> > >>> > Best regards >>> > Tim D=C3=BCsterhus >>>=20 >>> Nick previously suggested having the get-hook's first return value c= ached; 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 th= at acceptable? (In the typical case, it would be the same as the curren= t $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->c= reatedAt; } >> private int $cachedResult; >> public int $totalBalance { get =3D> $this->cachedResult ??=3D 5+1= 0; } >> 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: >> 1. Computed Properties (elapsedTimeSinceCreation): these are propert= ies of the object that are relevant to the object in question, but are n= ot static. In this case, you are not writing to the object. It is still = "readonly". >> 2. Memoization (expensiveCalculation): only calculate the property o= nce and only once. This is a performance optimization. It is still "read= only". >> 3. External State (accessLevel): properties of the object that rely = on some external state, which due to architecture or other convienence m= ay 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 p= osit that these are properties of the user object. In other words, a fun= ction to get these values would probably be named `getElapsedTimeSinceCr= eation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be writing get= ters anyway. >>=20 >> =E2=80=94 Rob > Hey Rob, >=20 > 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 examp= les would be cached. > A later call to the property would *not* use the updated time or a pot= entially updated external state. >=20 > After thinking a lot about it over the last days I think that makes se= nse.=20 >=20 > To stick to your usage of `time()`, I think the following is a good ex= ample: >=20 > ```php > readonly class JobHelper > { > public function __construct( > public readonly string $uniqueRunnerKey { > get =3D> 'runner/' . date("Ymd_H-i-s", time()) . '_' . (st= ring) random_int(1, 100) . '/'. $this->uniqueRunnerKey; > } > ) {} > } >=20 > $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 > ``` >=20 > 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`. >=20 > 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 re= turn the exact same value on consecutive calls. >=20 > If users don=E2=80=99t want that, they can opt-out by not using `reado= nly`. The guarantee only holds in combination with `readonly`. > Alternatively, as you proposed, using methods (which I think would rea= lly be a better fit; alternatively virtual properties which also will no= t support `readonly`. >=20 > With what we have now, both =E2=80=9Ccamps" will be able to achieve wh= at they want transparently. > And I believe that=E2=80=99s a good middle ground we should go forward= with. >=20 > *Cheers,* > Nick Hey Nick, After sleeping on it, I think I agree with this assessment. For backed p= roperties, 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. =E2=80=94 Rob --3209718a4c3c4143abf2ff13c73e62b6 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable


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=C3=BCsterhus wrote:
> Hi
><= /div>
> Am 2025-07-08 17:32, schrieb Nicolas Grekas:
&g= t;> I also read Tim's argument that new features could be stricter. I= f 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. Thi= s is the only real way to enforce readonly-ness in PHP.
>
> Making the class final still would not allow to optimize b= ased on the 
> fact that the identity of a value store= d in a readonly property will not 
> change after succ= essfully reading from the property once. Whether or not 
= > a property hooked must be considered an implementation detail, sinc= e a 
> main point of the property hooks RFC was that h= ooks can be added and 
> removed without breaking comp= atibility for the user of the API.
>
>> eng= ine-assisted strictness in this case. You cannot write such code in = ;
>> a
>> non-readonly way by mistake, s= o 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&nb= sp;
>> is
>> written as such in the orig= inal RFC, but the concrete definition and
>> implementat= ion of readonly isn't: you can set mutable objects to 
&g= t;> readonly
>> properties, and that means even reado= nly classes/properties are 
>> mutable, in
>> the generic case.
>
> `readonly` gu= arantees the immutability of identity. While you can 
>= ; certainly mutate mutable objects, the identity of the stored object&nb= sp;
> doesn't change.
>
> Best re= gards
> Tim D=C3=BCsterhus

Nick pr= eviously suggested having the get-hook's first return value cached; it w= ould still be subsequently called, so any side effects would still happe= n (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 acc= eptable?  (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 getter= s on readonly classes. Take this example for discussion:

<= /div>
readonly class User {
    public int = $elapsedTimeSinceCreation { get =3D> time() - $this->createdAt; }<= /div>
    private int $cachedResult;
 =    public int $totalBalance { get =3D> $this->cachedResu= lt ??=3D 5+10; }
    public int $accessLevel { = get =3D> getCurrentAccessLevel(); }
    publ= ic function __construct(public int $createdAt) {}
}
=
$user =3D new User(time() - 5);
var_dump($user-= >elapsedTimeSinceCreation); // 5
var_dump($user->totalBa= lance); // 15
var_dump($user->accessLevel); // 42

In this example, we have three of the most common ones:<= /div>
  1. Computed Properties (elapsedTimeSinceCreation): these are p= roperties 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 calcu= late the property once and only once. This is a performance optimization= . It is still "readonly".
  3. External State (accessLevel): properti= es of the object that rely on some external state, which due to architec= ture or other convienence may not make sense as part of object construct= ion. It is still "readonly".
You can mix-and-match these t= o provide your own level of immutability, but memoization is certainly n= ot the only one. 

You could make the argum= ent that these should be functions, but I'd posit that these are propert= ies of the user object. In other words, a function to get these values w= ould 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 `re= adonly` property value to never change after `get` was first called.
As you can see in my earlier mails I also was of a different opin= ion. 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 im= plementations your examples would be cached.
A later call to t= he property would *not* use the updated time or a potentially updated ex= ternal state.

After thinking a lot about it ove= r the last days I think that makes sense. 

To stick to your usage of `time()`, I think the following is a good exa= mple:

```php
readonly class J= obHelper
{
public function __con= struct(
public readonly string $uniqueRunnerKey {
= get =3D> 'runner/' . <= span style=3D"color:rgb(0, 92, 197);">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->uniqu= eRunnerKey;
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 h= old 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 us= ing `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 als= o 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 gro= und we should go forward with.

Cheers,
Nick

Hey Nick,
=
After sleeping on it, I think I agree with this assessmen= t. For backed properties, especially, it makes sense that the value feel= s "immutable". If and when we get to virtual properties, maybe not. But = that's a bridge to cross later.

=E2=80=94 Rob
--3209718a4c3c4143abf2ff13c73e62b6--