Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128004 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 A0F501A00BC for ; Fri, 11 Jul 2025 06:03:18 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752213688; bh=iYa+FmkSnCbPAHVDTcQN6T2dZ+DpwSto1HzfAG0EfK8=; h=Date:From:To:Cc:In-Reply-To:References:Subject:From; b=lKoqacPvVK2ajGGT0L49cWdBjnZT2usIgHsAy9lG1VPexVWrSQikQmhpqm9lIqNOh kvTsSMp+vd0VxHSZ7e/1Abut55B06q5J7OkA4lf72E6aNdRYnblOboEszzEOEPNzX0 qyXfHmKbqlZYkNSdgVYB4TkozyUpHLD9BLAHFTDzTHpyKXcUnw3Tf4gojaRzHM1BDr 2zfhkwS6p5ISeU9MAdTl2TagSoPqxJh4B9g9smCxfGdbpjLYyResG/+lCxMUWANuci sDZE7Dk1ZQ+AMwVhdy4p0BOAgfUkOIg8zJ3E7m9Cti7EqxaoP00dEicnqQ8ODq7GKF bzQomgHBtkP2Q== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 9A2B81807A7 for ; Fri, 11 Jul 2025 06:01: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=-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-a2-smtp.messagingengine.com (fout-a2-smtp.messagingengine.com [103.168.172.145]) (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 06:01:27 +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 54FF7EC0213; Fri, 11 Jul 2025 02:03:16 -0400 (EDT) Received: from phl-imap-05 ([10.202.2.95]) by phl-compute-05.internal (MEProxy); Fri, 11 Jul 2025 02:03:16 -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=1752213796; x= 1752300196; bh=kyM2DNiAOSMnXt1W6L8dBB3o+PZ9DJ5ttS/9d0rnjOk=; b=W 9Yq7xZxVGAfvHt67i6QxO4AbwCddFwHi5ATcUScp3+zIMOlWnwlFKcHEADVKrpZ4 9i9x3f6aCCUJzFkaRY6uPjbVY42Yky0d2JaglcE37QV6y9TI4LjYdIZ/INs0Y2W5 GgP2/uuiW1JgL52xp2CAPEDt6tuDxviHCFl9zrQCHW8lb2cJbbHwR5iWS6/VlJEH jkncBJnms91hmDTkOuIo8yHZy2ieZ7QmMCatOqiZ4a51z8FgN3yoemxXtF25l1Ly HQTjEt+qIY7nMZm9xZHDGACF/dZF2S7NRW8kiGxnBlo41aIOMpR/se5209AWvnV3 dO/Y8GYvOJS7L78iCW/Og== 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= 1752213796; x=1752300196; bh=kyM2DNiAOSMnXt1W6L8dBB3o+PZ9DJ5ttS/ 9d0rnjOk=; b=CDKzSkI6sxqhrhm6K8VL4dABFl2lA9xx4k8UvvK7zAcrQkETx8O UJGVKx4xnIT+L8bPVkR/jfR7Kj0EIvA+Ps1am+JLatxotvDLB6bLCpGYjqjvv8sm Bm0XKWa3stMrAcLx8cdxlUuj8jR4Z0z4VE++aeCJIChSOCJ8KJXjZuVdeAs87uQk pFb/wta+7QTT5Li0Ze2G9xTXE3MFQULV5Q8xVEME4E53PQNPwpDxcHPqqb1nOYJd QkgM0IVfYhLWWyfyvzaZV8uwvAxcLwwquzixv9VG/gOLoaeiq/Pr3mglev4GBh6f M7HSXmIyjebWOX+xhtIxq59gZ8TUY+/6G1w== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdefgdegvdehhecutefuodetggdotefrod 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 952B81820074; Fri, 11 Jul 2025 02:03:15 -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 08:02:30 +0200 To: Nick Cc: internals@lists.php.net Message-ID: 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=b1e53b397e894779a95aae2525d913b4 From: rob@bottled.codes ("Rob Landers") --b1e53b397e894779a95aae2525d913b4 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable On Fri, Jul 11, 2025, at 02:40, Nick wrote: > Hey Rob, >=20 >> On 11. Jul 2025, at 01:43, Rob Landers wrote: >>>=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 >=20 > Please remember that the RFC will allow `readonly` on backed propertie= s only, not on virtual hooked properties.=20 > Nothing from your example would work, and it would result in: > Fatal error: Hooked virtual properties cannot be declared readonly > My proposed alternative implementation with caching addresses the conc= ern Claude and Tim had and will make this hold: >=20 > ```php > class Unusual > { > public function __construct( > public readonly int $value { > get =3D> $this->value * random_int(1, 100); > } > ) {} > } >=20 > $unusual =3D new Unusual(1); > var_dump($unusual->value =3D=3D=3D $unusual->value); // true ``` >=20 > =E2=80=93 Nick Hey Nick, I was merely illustrating the different types, you can simply replace th= em with non-virtual examples. To your idea of forced memoization, I'm no= t sure that's a good idea. In cases where its a single execution context= per request, it may be fine, but in cases of worker mode on frankenphp,= where readonly objects may live for quite awhile, it may not be. Genera= lly, memoization is an optimization, not a property of a value. In your example above, this breaks the principle of least astonishment, = and potentially violates referential transparency due to the calculation= being non-pure. Referential transparency basically says we can can repl= ace the use of the variable with it's value (in this case, $ususual->val= ue with $unusual->backing_value * random_int(1, 100)). If it is memoized= , we cannot. There isn't a way to express that it is one value the first= time (random) and a different value the next time (the previous computa= tion). This is a bit dangerous because the programmer has no way to reli= ably reason about the code as if the expression can be substituted freel= y. =E2=80=94 Rob --b1e53b397e894779a95aae2525d913b4 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable
On Fri, Jul = 11, 2025, at 02:40, Nick wrote:
Hey Rob,

On 11. Jul 2025, at 01:43, Rob Lander= s <rob@bottled.codes> wrote:

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 ret= urned.  Would anyone find that acceptable?  (In the typical ca= se, it would be the same as the current $this->foo ??=3D compute() pa= ttern, just with an extra cache entry.)

--Larry= Garfield


I think t= hat 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; }
    privat= e int $cachedResult;
    public int $totalBalan= ce { get =3D> $this->cachedResult ??=3D 5+10; }
 &n= bsp;  public int $accessLevel { get =3D> getCurrentAccessLevel()= ; }
    public function __construct(public int = $createdAt) {}
}

$user =3D new User(t= ime() - 5);
var_dump($user->elapsedTimeSinceCreation); // 5=
var_dump($user->totalBalance); // 15
var_dump($u= ser->accessLevel); // 42

In this example, we= have three of the most common ones:
  1. Computed Properties (e= lapsedTimeSinceCreation): these are properties of the object that are re= levant to the object in question, but are not static. In this case, you = are not writing to the object. It is still "readonly".
  2. Memoizati= on (expensiveCalculation): only calculate the property once and only onc= e. This is a performance optimization. It is still "readonly".
  3. E= xternal State (accessLevel): properties of the object that rely on some = external state, which due to architecture or other convienence may not m= ake sense as part of object construction. It is still "readonly".
  4. You can mix-and-match these to provide your own level of immutab= ility, 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 wo= rds, a function to get these values would probably be named `getElapsedT= imeSinceCreation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be w= riting getters anyway.

    =E2= =80=94 Rob

Please reme= mber that the RFC will allow `readonly` on backed properties only, not o= n virtual hooked properties. 
Nothing from your example w= ould work, and it would result in:
Fatal error: Hooked virtual properties=
 cannot be declared readonly
My proposed alternati= ve implementation with caching addresses the concern Claude and Tim had = and will make this hold:

```php
class Unusual
{
public function __construct(
public readonly int $value {
= get =3D> $this->= ;value * random_int(1, 100);
}
) {}
= }

$unusual =3D new Unusua= l(1);
var_dump($unusual->value =3D= =3D=3D $unu= sual->value); // true
```
<= /div>

=E2=80=93 Nick

Hey Nick,

I was merely illustrating the d= ifferent types, you can simply replace them with non-virtual examples. T= o your idea of forced memoization, I'm not sure that's a good idea. In c= ases where its a single execution context per request, it may be fine, b= ut in cases of worker mode on frankenphp, where readonly objects may liv= e for quite awhile, it may not be. Generally, memoization is an optimiza= tion, not a property of a value.

In your exampl= e above, this breaks the principle of least astonishment, and potentiall= y violates referential transparency due to the calculation being non-pur= e. Referential transparency basically says we can can replace the use of= the variable with it's value (in this case, $ususual->value with $un= usual->backing_value * random_int(1, 100)). If it is memoized, we can= not. There isn't a way to express that it is one value the first time (r= andom) and a different value the next time (the previous computation). T= his is a bit dangerous because the programmer has no way to reliably rea= son about the code as if the expression can be substituted freely.
=

=E2=80=94 Rob
--b1e53b397e894779a95aae2525d913b4--