Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127999 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 E76381A00BC for ; Fri, 11 Jul 2025 00:40:44 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752194334; bh=Fks7GfBfoqd4aKBZE9LzxUQcW/uEQLKNZNj2gEB0u1s=; h=From:Subject:Date:In-Reply-To:Cc:To:References:From; b=DsouvA6oEYK+qjZLS8XjSllg06Hng8T8RuI9X7pDdaoiJ8AW65+DKtJ1J/i/A7bEP TuTlD3guPKcSA8Gw11TTOwDB3Z2KEQMTHVeXsu1PQN3HgdBwtlJPyCOg71kUAoShY+ yyJknKsFrl1qPU76UtuxsFVqP5VqYxj7ivf83awr/4c7KAhwdkTCJ+rT6wuMOsZzWT uDBHeJE8rnB97WXOSXCEQFs7VnGkRA12QXtZEq9aamDx+zqUTFNv0SYBxPsd0oBtk9 0YT2nRQXklfhQDkn3Jxv0KkbmYdXUiIUMt3F0u4rRdej1WoD/0/r8koFkujcoHXjDs 9cntjJT4bgTlw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 86D4918005D for ; Fri, 11 Jul 2025 00:38:53 +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.1 required=5.0 tests=BAYES_00,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 00:38:52 +0000 (UTC) Received: from avril.gn2.hosting (localhost [127.0.0.1]) by avril.gn2.hosting (Postfix) with ESMTP id 6669D1C4082C; Fri, 11 Jul 2025 02:40:39 +0200 (CEST) Received: from smtpclient.apple (unknown [111.68.29.103]) by avril.gn2.hosting (Postfix) with ESMTPSA id 80FA51C405DB; Fri, 11 Jul 2025 02:40:38 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=nicksdot.dev; s=default; t=1752194439; 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=NMTDDxXEoHoP2/uUTG45NQ79XZiaqMs8E/vm7Y0tB2Q=; b=GRR7VFUy1gk+a5TU7w02IAv7E1pvCZZaKgKrSBbIAO+8M2Yo2HNmcyjGSduvLN5t0t2o7D vZvJzefZglb62t77VTQCzXz/9jkKfUuBQnGU8j4ef1lC5KMAp5UWZIOfBqur72Q03ynMNX LnILJZH/C+sha1rUFlsUVOQw+BVylnJ9SE4z5os7x8Rkid8fvI3NfMrkK0h1Tm/3CZRJ+U hru3RhMwImAwRMPE5jKShD7JioTMoRWp4+b/jFzPwmMbYhLywzVmADgLjnj2vPX7qRSD1E dRYEnCjwCCjGNJZ9Unu4UIS+3PvwLi2IJMkOyeAbLR61sOD0C3HW2mwEq9rmhA== Message-ID: Content-Type: multipart/alternative; boundary="Apple-Mail=_511F70F8-5BB1-4608-A112-C7F8D04C8591" 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 07:40:21 +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=_511F70F8-5BB1-4608-A112-C7F8D04C8591 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 Hey Rob, > On 11. Jul 2025, at 01:43, Rob Landers wrote: >>=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 Please remember that the RFC will allow `readonly` on backed properties = 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 = 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 Unusual(1); var_dump($unusual->value =3D=3D=3D $unusual->value); // true=20 ``` =E2=80=93 Nick= --Apple-Mail=_511F70F8-5BB1-4608-A112-C7F8D04C8591 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8 Hey Rob,

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

Please remember that the RFC = will allow `readonly` on backed properties only, not on virtual hooked = properties. 
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 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 Unusual(1);
var_dump($unusual->value =
=3D=3D=3D=
 $unusual->value); // true 
```

=E2=80=93 = Nick
= --Apple-Mail=_511F70F8-5BB1-4608-A112-C7F8D04C8591--