Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128023 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 D30201A00BC for ; Sun, 13 Jul 2025 19:56:50 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752436502; bh=tIDzuc1cweEGbchF0i6BteVfYzmFCjMyukWxmxY39Oc=; h=From:Subject:Date:In-Reply-To:Cc:To:References:From; b=MEB38K0cjml/4xQ27iuGlR3CKYJyRs1UwfS26u4uABqrqI3vfdb10AKre/Vv9GWeQ Mor50BRzDUCsHiYdNYO0wTjNe3UBhGBxsznYFOYpAYziqU5y8b2zGitxRs/mC8tmcP lOV//FnR7dI82xhqDLmo1mb4OQxUFdUJim6d2xfwKamcqgwLfyF4IpK+lWsILDrj7Q c5hsGo/w617QkYLKEihtZb2pN+wa07sm0Qc1wYHxdnPmfiTHGLmzrzvUOqzCEUDz7K O+8N5NFLaR6Ge6fF8Vk+UrwyhTuXnHl36fv9i6mtswa8PHETF+igyXPtwC4EdenDYK DCa3XgAeFR0Nw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id EA02F18005B for ; Sun, 13 Jul 2025 19:54:59 +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.2 required=5.0 tests=BAYES_40,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 ; Sun, 13 Jul 2025 19:54:59 +0000 (UTC) Received: from avril.gn2.hosting (localhost [127.0.0.1]) by avril.gn2.hosting (Postfix) with ESMTP id D00941C40C1D; Sun, 13 Jul 2025 21:56:45 +0200 (CEST) Received: from smtpclient.apple (unknown [111.68.29.103]) by avril.gn2.hosting (Postfix) with ESMTPSA id BC4211C409DD; Sun, 13 Jul 2025 21:56:40 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=nicksdot.dev; s=default; t=1752436601; 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=tkdSiz9Xbu0+994+bfZHUZM+bRdvaRwfem3SYcHbyF8=; b=ucblFQLBKQTcK7fGs2CBOM8h3QylJ4PEga5wPFVvWTB8QWuX5dyXHT8ZRPOTXtGAJf36m1 pXKAycMTbPJfvYoW5lSmTaA7LnlNDp57boDBdFGoiNjVw47bjLQT5yQRO4qjrgIjqOD4vU nD9zXLPzAmMIq/rSQS7YiwSPTBGRYO0bnh2iF9oNPNK2JXqKF/eRzDiVkJAq6vVLsM/DO0 LQoC6tCZWojpUGZsyeh/UtWp6qzuCZu5Pyaqk4lu8jGvHLWrpl2Mk12haPAqkMHL07O86w EP7RAzx0mKdHVAmDE+0poBgwpF6ss8YRgzQU0rbnf3ugvqxztRTjTk45R71brA== Message-ID: <97C001B0-05DC-4797-A72A-F82E1A0AC30F@nicksdot.dev> Content-Type: multipart/alternative; boundary="Apple-Mail=_27337BA9-9EDB-4057-A7D6-75E00797EDFB" 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: Mon, 14 Jul 2025 02:56:22 +0700 In-Reply-To: Cc: Marc Bennewitz , internals@lists.php.net To: erictnorris@gmail.com References: <1e8634d7-ac1a-4025-b4e2-1948aabf5251@app.fastmail.com> <9D5043B2-1589-4FD5-B289-6E98FB1177BE@nicksdot.dev> <16BD443D-A179-465D-84A0-6E3780F62D8E@gmail.com> <203C1BD7-E688-4E26-8EA6-EA5331525470@nicksdot.dev> <71E5E8FE-605D-4B32-8CCD-F7ECBD454E24@nicksdot.dev> X-Mailer: Apple Mail (2.3826.600.51.1.1) From: php@nicksdot.dev (Nick) --Apple-Mail=_27337BA9-9EDB-4057-A7D6-75E00797EDFB Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 > On 14. Jul 2025, at 01:15, Eric Norris = wrote: >=20 > On Sun, Jul 13, 2025 at 2:00=E2=80=AFPM Marc Bennewitz = wrote: >>=20 >>=20 >> On 13.07.25 18:17, Nick wrote: >>=20 >>=20 >> On 13. Jul 2025, at 20:38, Marc Bennewitz wrote: >>=20 >> Hi Nick, Claude, >>=20 >> Hey Marc, >>=20 >> I think it's important to explicitly mark it as "this value will be = stored in memory", I mean just silently caching the get hook could = quickly lead to unexpected behavior. Like one would expect the value to = be changed >>=20 >> The most here made the argument that "a changing value from a = readonly get hook" would be the unexpected behaviour. >> That=E2=80=99s why we ended up with the current =E2=80=9Calternative = implementation 2=E2=80=9D. Please see my last answer to Rob for a fair = example [1] . >>=20 >> The current preferred alternative implementation covers both = situations... >>=20 >> If a property is `readonly`: >> - you can set once >> - on read you always get the same (once computed) value back >>=20 >> This is exactly the behavior I mean which is somehow unexpected if = not marked explicitly as the result could be cached and the property = value will never change. >>=20 >> Not being able to write to something doesn't generally mean reading = the value will never change. `get =3D> random_int(0, 100);` >>=20 >> I don't want to say that the path we want to go with readonly being = able to assume the value will never change is wrong but I think it's not = clear for the user and can be missed quickly - even with a test as you = have to read the property multiple time to notice the difference. >>=20 >>=20 >> If a property is NOT `readonly`: >> - you can set often >> - on read you always get the fresh (often computed) value back >>=20 >> I argue that this is a very easy mental model. >> I hope that voters agree on =E2=80=9Ccached may be implied by = readonly=E2=80=9D, as Claude called it. >>=20 >> All what I'm saying is that this behavior should be explicit and not = applied implicitly on a readonly get hook. >=20 > I agree with Marc here, not surprisingly. >=20 >> To have an `init` hook doesn=E2=80=99t solve the get hook issue. >>=20 >> As far as I understood the init hook it would still disallow = readonly+get but allow readonly+init and init would be called once the = first time the property gets read and the result would end up in the = backing store. >>=20 >> As a result you get the same behavior as `cached get` with the only = difference that you write `init =3D> random_int();` instead of `cached = get =3D> random_int();` >=20 > Agreed. Nick, I am not sure how the init hook doesn't "solve the get > hook issue". As I understand it, the get hook issue is that get hooks > are not allowed on readonly properties. The init hook would be > identical to a "cached get" hook on a readonly property, so why > doesn't it solve the issue? >=20 >> Or did I misunderstand it? >=20 > I share the same understanding as you, Marc. >=20 >> The cached modifier I would expect to be an attribute applicable to = any function which uses another cache store similar to how it's possible = in python to memorize function calls which would be a very different = feature. >>=20 >> As earlier answered to Claude [2], I seek to write less code. To = introduce a `cached` modifier voids this for no strong reason (please = see =E2=80=9Cmental model=E2=80=9D above). >>=20 >> See above - and `init` is just once more character. >=20 > I was going to respond to this point earlier, but Marc beat me to it. > An "init" hook is one more character than a get hook, is explicit over > a get hook that works differently only for readonly properties, *and* > is far fewer characters than the explicit "cached" modifier get hook > option. >=20 > On top of that, as Claude mentioned an init hook provides the ability > to differentiate between a null property and an uninitialized property > - an init hook would only be called for uninitialized properties, so > no need for $this->foo ??=3D "bar=E2=80=9D. Hey Marc, Hey Eric, A) Init hook Marc,=20 > Or did I misunderstand it? Well, I don=E2=80=99t know. Everyone seems to think of init hooks (and = their playing together with other hooks) differently. Some say this, some say that. That=E2=80=99s the exact issue. Want an = example? Eric just agreed with your code example which has a get hook AND init = hook. >> ``` >> class Test { >> public int $seek { >> init =3D> random_int(0, 100); // called once on read if not = initialized >> get =3D> $this->seek + 100; >> set =3D> $this->seek =3D $value + 100; >> } >> } >> ``` >> var_dump((new Test())->seek); // number between 100-200 OR 200-300 = depending of the set hook be called once with the result of init as = well. >> Or did I misunderstand it?=20 >=20 > I share the same understanding as you, Marc. But one mail before he answered to Larry: > I think, at least for readonly, you couldn't have an init hook and a > get hook, since the main objection here is to having get hooks on > readonly properties at all. On normal properties, I think that'd be > okay? So what is it? Get hook cool, or not? And how does an init hook work = exactly?=20 How play all combinations together? And how with readonly?=20 Why didn=E2=80=99t your example use readonly if we talk about readonly = hooks? I don=E2=80=99t know all that. And that=E2=80=99s why I have proposed = what I proposed. We have a reasonable solution for set/get, without init, right in front = of us; and millions of devs could benefit from it the next release. Eric,=20 why it wouldn=E2=80=99t be solved by an init hook? Well, because as you = said, readonly get hooks would not be allowed in combination with init.=20= Others apparently have different opinions. So will they, will they not? I, however, want get/set on readonly properties. And that is what I = proposed here.=20 An init hook is not part of this proposal and I am not planning to take = this on.=20 This RFC, however, would not block anyone from creating their own RFC = for init hooks. B) Less Code Marc was talking about init, cache modifier and attributes in the same = time. Claude initially wanted a cache modifier on each hook. I argue that =E2=80=9Creadonly implicates cached=E2=80=9D is very = reasonable here. Many, many opinions. Many, many options. All have their pros, and cons. = All can be attacked, and defended. :) All I want is the below (less code; and no dealing with unrelated things = just because I want to add hooks to a readonly class).=20 ```php // I have a nice readonly class final readonly class Entry { public function __construct( public string $word, public string $slug, ) {} } // I simply want to add a hooked-property to that readonly class final readonly class Entry { public function __construct( public string $word, public string $slug, public array $terms { set(array $value) =3D> array_map(static function (Term|array = $term): Term { return $term instanceof Term ? $term : new = Term(...$term); }, $value); get =3D> $this->terms; // something, something }, ) {} } // but I cannot. I need to: // - make the class non-readonly, // - add some readonly here and there, // - deal with async visibility // to eventually end up with this final class Entry // cannot be readonly, annoying { public function __construct( public readonly string $word, // annoying readonly public readonly string $slug, // annoying readonly private(set) array $terms { // requires visibility set(array $value) =3D> array_map(static function (Term|array = $term): Term { return $term instanceof Term ? $term : new = Term(...$term); }, $value); get =3D> $this->terms; }, ) {} } ``` Adding hook to a readonly class really should not be THAT hard and = demanding. And for that, I believe, I provided a solution that is easy to reason = about (all details in previous mails), and allows everyone to achieve = what they want. Cheers, Nick= --Apple-Mail=_27337BA9-9EDB-4057-A7D6-75E00797EDFB Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8
On 14. Jul = 2025, at 01:15, Eric Norris <eric.t.norris@gmail.com> = wrote:

On Sun, = Jul 13, 2025 at 2:00=E2=80=AFPM Marc Bennewitz <marc@mabe.berlin> = wrote:


On 13.07.25 18:17, Nick = wrote:


On 13. Jul 2025, at 20:38, Marc Bennewitz = <marc@mabe.berlin> wrote:

Hi Nick, Claude,

Hey = Marc,

I think it's important to explicitly mark it as "this value = will be stored in memory", I mean just silently caching the get hook = could quickly lead to unexpected behavior. Like one would expect the = value to be changed

The most here made the argument that "a = changing value from a readonly get hook" would be the unexpected = behaviour.
That=E2=80=99s why we ended up with the current = =E2=80=9Calternative implementation 2=E2=80=9D. Please see my last = answer to Rob for a fair example [1] .

The current preferred = alternative implementation covers both situations...

If a = property is `readonly`:
- you can set once
- on read you always = get the same (once computed) value back

This is exactly the = behavior I mean which is somehow unexpected if not marked explicitly as = the result could be cached and the property value will never = change.

Not being able to write to something doesn't generally = mean reading the value will never change. `get =3D> random_int(0, = 100);`

I don't want to say that the path we want to go with = readonly being able to assume the value will never change is wrong but I = think it's not clear for the user and can be missed quickly - even with = a test as you have to read the property multiple time to notice the = difference.


If a property is NOT `readonly`:
- you can set = often
- on read you always get the fresh (often computed) value = back

I argue that this is a very easy mental model.
I hope = that voters agree on =E2=80=9Ccached may be implied by readonly=E2=80=9D, = as Claude called it.

All what I'm saying is that this behavior = should be explicit and not applied implicitly on a readonly get = hook.

I agree with Marc here, not = surprisingly.

To have an `init` hook = doesn=E2=80=99t solve the get hook issue.

As far as I understood = the init hook it would still disallow readonly+get but allow = readonly+init and init would be called once the first time the property = gets read and the result would end up in the backing store.

As a = result you get the same behavior as `cached get` with the only = difference that you write `init =3D> random_int();` instead of = `cached get =3D> random_int();`

Agreed. Nick, I = am not sure how the init hook doesn't "solve the get
hook issue". As = I understand it, the get hook issue is that get hooks
are not allowed = on readonly properties. The init hook would be
identical to a "cached = get" hook on a readonly property, so why
doesn't it solve the = issue?

Or did I misunderstand = it?

I share the same understanding as you, = Marc.

The cached modifier I would = expect to be an attribute applicable to any function which uses another = cache store similar to how it's possible in python to memorize function = calls which would be a very different feature.

As earlier = answered to Claude [2], I seek to write less code. To introduce a = `cached` modifier voids this for no strong reason (please see =E2=80=9Cmen= tal model=E2=80=9D above).

See above - and `init` is just once = more character.

I was going to respond to this point = earlier, but Marc beat me to it.
An "init" hook is one more character = than a get hook, is explicit over
a get hook that works differently = only for readonly properties, *and*
is far fewer characters than the = explicit "cached" modifier get hook
option.

On top of that, as = Claude mentioned an init hook provides the ability
to differentiate = between a null property and an uninitialized property
- an init hook = would only be called for uninitialized properties, so
no need for = $this->foo ??=3D = "bar=E2=80=9D.

Hey Marc, Hey = Eric,

A) Init = hook

Marc, 

Or did I misunderstand = it?

Well, I don=E2=80=99t know. = Everyone seems to think of init hooks (and their playing together with = other hooks) differently.
Some say this, some say that. = That=E2=80=99s the exact issue. Want an = example?

Eric just agreed with your code = example which has a get hook AND init = hook.

```
class Test {
    public int $seek = {
        init =3D> = random_int(0, 100);   // called once on read if not = initialized
        get =3D> = $this->seek + 100;
        set = =3D> $this->seek =3D $value + 100;
    = }
}
```
var_dump((new = Test())->seek); // number between 100-200 OR 200-300 depending of the = set hook be called once with the result of init as = well.
Or did I misunderstand = it? 

I share the same understanding as you, = Marc.

But one mail before he = answered to Larry:

I think, at = least for readonly, you couldn't have an init hook and a
get hook, = since the main objection here is to having get hooks on
readonly = properties at all. On normal properties, I think that'd = be
okay?

So what is it? Get = hook cool, or not? And how does an init hook work = exactly? 
How play all combinations together? And how = with readonly? 
Why didn=E2=80=99t your example use = readonly if we talk about readonly hooks?

I = don=E2=80=99t know all that. And that=E2=80=99s why I have proposed what = I proposed.

We have a reasonable solution for = set/get, without init, right in front of us; and millions of devs could = benefit from it the next = release.

Eric, 
why it = wouldn=E2=80=99t be solved by an init hook? Well, because as you said, = readonly get hooks would not be allowed in combination with = init. 
Others apparently have different opinions. So will = they, will they not?

I, however, want = get/set on readonly properties. And that is what I proposed = here. 

An init hook is not part of this = proposal and I am not planning to take this on. 
This = RFC, however, would not block anyone from creating their own RFC for = init hooks.

B) Less = Code

Marc was talking about init, cache = modifier and attributes in the same time.
Claude initially = wanted a cache modifier on each hook.

I argue = that =E2=80=9Creadonly implicates cached=E2=80=9D is very reasonable = here.

Many, many opinions. Many, many options. = All have their pros, and cons. All can be attacked, and defended. = :)

All I want is the below (less code; and no = dealing with unrelated things just because I want to add hooks to a = readonly = class). 

```php

// I have a nice readonly = class
final readonly = class Entry
{
=
public function __construct(
public = string $word,
=
public string $slug,
) = {}
}
// I simply want to add a = hooked-property to that readonly class
final readonly class Entry
{
=
public function __construct(
public = string $word,
=
public string $slug,
public array $terms {
set(array $value) = =3D> array_map(static function = (Term|array $term): Term {
return = $term instanceof Term = ? $term : = new Term(...$term);
}, $value);
=
get =3D> $this->terms; = // something, = something
},
) = {}
}
// but I cannot. =
I need to:
// - = make the class non-readonly,
// - add some readonly here and there,
// - deal with async = visibility
// to = eventually end up with this
final class Entry = // cannot be readonly, = annoying
{
public function __construct(
=
public readonly string $word, // annoying readonly
public = readonly string $slug, = // annoying = readonly
private(set) array = $terms { // requires visibility
set(array $value) = =3D> array_map(static function = (Term|array $term): Term {
return = $term instanceof Term = ? $term : = new Term(...$term);
}, $value);
=
get =3D> $this->terms;
},
) = {}
}
```

Adding hook to a readonly class really should not be THAT hard and = demanding.

And for that, I believe, I provided = a solution that is easy to reason about (all details in previous mails), = and allows everyone to achieve what they = want.


Cheers,
Nic= k
= --Apple-Mail=_27337BA9-9EDB-4057-A7D6-75E00797EDFB--