Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128093 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 A657B1A00BC for ; Thu, 17 Jul 2025 07:00:22 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752735515; bh=iXO9l3qpqffKAWuq+cojBCVJDdtDFR5m0E4qQd74uB4=; h=Date:From:To:Cc:In-Reply-To:References:Subject:From; b=Y/6BqN3r6ItTEe708UGnB8hiElN8Jb5Ifr51UAnV+304YiKkj6YPTh11WkVEBtDov zK0Qxd+1o9kVj8dPnru9BWB8OErXMOvNbxPsdGdKqrezgsHip0NtTCaQGke0hUTzVt 0vrZZECY4GA0ETpoPmbKLLtUIhjkN3ECfsdlrYyv4rbiRL+BUGSTm+sHDNgG8cXLOi 7C5KmwWU/gLMZ8Nf20hd/weFGVzLnsKh702yhNurpSOYb4cYLDrlp2vuv9aby3zqSU wSr00SXDkTkvr7YpTxEfmRha+s3syDCHk0SxuHg27r3b4z8rrsv42bg6TYVj5U2UV1 /ktRblGEDRHkA== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 405011804E2 for ; Thu, 17 Jul 2025 06:58:34 +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-b6-smtp.messagingengine.com (fout-b6-smtp.messagingengine.com [202.12.124.149]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Thu, 17 Jul 2025 06:58:24 +0000 (UTC) Received: from phl-compute-05.internal (phl-compute-05.phl.internal [10.202.2.45]) by mailfout.stl.internal (Postfix) with ESMTP id E088F1D00192; Thu, 17 Jul 2025 03:00:10 -0400 (EDT) Received: from phl-imap-05 ([10.202.2.95]) by phl-compute-05.internal (MEProxy); Thu, 17 Jul 2025 03:00:11 -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=1752735610; x= 1752822010; bh=neMu0K1P4J7HJ1dGXpJLU+FqRyoe1yDJ14/YQqu5XXo=; b=k ZsJyue5gj1g5TrF60qd8DLtxMrw8e9KUbVWh8aR9IoaPw3uK3YwrbOaZzy4v6asV vzZeUqQaNLFhXPodYnmDVNuyz68O5/JZHeseq0OUWjWYnsKj/VGThbGzMiX+AufR STxJasr2wLMAIPpskhARQSeYvnZINYqU3Eho5zUGQHqWKpLY6Dc+6oATe6+YaXl2 1kk8XWx1mTRd2emJcCGK8awL3Y8b7W7pxJCL89xcIJsCrJhh9Lc5Mj55EymugXEp bcuWMz+Ab1J5Z8Hy/eoFzLoNSSp5AJINcQguuyHlsQWlJM44B+2+m4j9O+4lwDry N5hRW15InRQBKKahFAm3g== 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= 1752735610; x=1752822010; bh=neMu0K1P4J7HJ1dGXpJLU+FqRyoe1yDJ14/ YQqu5XXo=; b=SBWI+BZVn7Qcxsf/lcHeMp/mfhv4fXdPCmqfxlLebo0hGvTPv+/ j4qH7TA26NmL42sxSBYjSeL3HvI3pJV4pPDUFsf5yfbgRdSJkg1/I+DSdO2R3sOw mo02uP+KHZUARbAl2O+DT0ckzD+DepCBZ2QQsBZ9sNlI+HobRhbVKKzC35FG4oaj LJ8pqmnzFLfOHY9vWxmZwg8JHLVEdjYFvdWOA5HrxfzEM7+nFT30r64imuyNSs25 XN0D+XcphuIa8sn3uu4Ri/dOF6PzCTiecHHbhkgHoW1rfPGcIz9LUdAqJ3lhdFrG DFSUw+9v7uJPhr9SPM16lNjPaky406GoMkA== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdefgdehleelgecutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpuffrtefokffrpgfnqfghnecuuegr ihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmdenucfjug hrpefoggffhffvvefkjghfufgtsegrtderreertdejnecuhfhrohhmpedftfhosgcunfgr nhguvghrshdfuceorhhosgessghothhtlhgvugdrtghouggvsheqnecuggftrfgrthhtvg hrnhepteefffegvdduleegkedvuedvhfeifffggfdvudejieektdeltdfgkeevfeeggfef necuffhomhgrihhnpehphhhprdhnvghtpdhgihhthhhusgdrtghomhenucevlhhushhtvg hrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehrohgssegsohhtthhlvggu rdgtohguvghspdhnsggprhgtphhtthhopeefpdhmohguvgepshhmthhpohhuthdprhgtph htthhopehlrghrrhihsehgrghrfhhivghlughtvggthhdrtghomhdprhgtphhtthhopehn ihgtohhlrghsrdhgrhgvkhgrshdophhhphesghhmrghilhdrtghomhdprhgtphhtthhope hinhhtvghrnhgrlhhssehlihhsthhsrdhphhhprdhnvght X-ME-Proxy: Feedback-ID: ifab94697:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 4602E1820074; Thu, 17 Jul 2025 03:00:10 -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: Thu, 17 Jul 2025 08:59:49 +0200 To: "Nicolas Grekas" , "Larry Garfield" Cc: "php internals" Message-ID: <4dee212b-55f9-4b7b-99c4-9fac4bc149cc@app.fastmail.com> In-Reply-To: References: <1e8634d7-ac1a-4025-b4e2-1948aabf5251@app.fastmail.com> <9D5043B2-1589-4FD5-B289-6E98FB1177BE@nicksdot.dev> <0856c89f-2000-448a-bbbf-c145a8699f6a@app.fastmail.com> Subject: Re: [PHP-DEV] [RFC] Readonly property hooks Content-Type: multipart/alternative; boundary=c6b5a68ff1e14308a9cbaa262c148972 From: rob@bottled.codes ("Rob Landers") --c6b5a68ff1e14308a9cbaa262c148972 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable On Tue, Jul 15, 2025, at 19:27, Nicolas Grekas wrote: >=20 >=20 > Le lun. 14 juil. 2025 =C3=A0 15:41, Larry Garfield a =C3=A9crit : >> On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote: >> > Hi Nick >> > >> > On Fri, Jul 11, 2025 at 6:31=E2=80=AFAM Nick wro= te: >> >> >> >>> On 8. Jun 2025, at 11:16, Larry Garfield = wrote: >> >>> >> >>> https://wiki.php.net/rfc/readonly_hooks >> >>> >> >>> To not get this buried in individual answers to others: >> >> >> >> I came up with two alternative implementations which cache the com= puted `get` hook value. >> >> One leverages separate cache properties, the other writes directly= to the backing store. >> >> >> >> Links to the alternative branches can be found in the description = of the original PR. >> >> https://github.com/php/php-src/pull/18757 >> > >> > I am not a fan of the caching approach. The implementation draft for >> > this approach [^1] works by storing the assigned value in the prope= rty >> > slot, and replacing it with the value returned from get one called = for >> > the first time. One of the issues here is that the backing value is >> > observable without calling get. For example: >> > >> > ``` >> > class C { >> > public public(set) readonly string $prop { >> > get =3D> strtoupper($this->prop); >> > } >> > } >> > $c =3D new C(); >> > $c->prop =3D 'foo'; >> > var_dump(((array)$c)['prop']); // foo >> > $c->prop; >> > var_dump(((array)$c)['prop']); // FOO >> > ``` >> > >> > Here we can see that the underlying value changes, despite the >> > readonly declaration. This is especially problematic for things like >> > [un]serialize(), where calling serialize() before or after accessing >> > the property will change which underlying value is serialized. Even >> > worse, we don't actually know whether an unserialized property has >> > already called the get hook. >> > >> > ``` >> > class C { >> > public public(set) readonly int $prop { >> > get =3D> $this->prop + 1; >> > } >> > } >> > $c =3D new C(); >> > $c->prop =3D 1; >> > $s1 =3D serialize($c); >> > $c->prop; >> > $s2 =3D serialize($c); >> > var_dump(unserialize($s1)->prop); // int(2) >> > var_dump(unserialize($s2)->prop); // int(3) >> > ``` >> > >> > Currently, get is always called after unserialize(). There may be >> > similar issues for __clone(). >> > >> > For readable and writable properties, the straight-forward solution= is >> > to move the logic to set. >> > >> > ``` >> > class C { >> > public public(set) readonly int $prop { >> > set =3D> $value + 1; >> > } >> > } >> > ``` >> > >> > This is slightly differently, semantically, in that it executes any >> > potential side-effects on write rather than read, which seems >> > reasonable. This also avoids the implicit mutation mentioned >> > previously. At least in these cases, disallowing readonly + get see= ms >> > reasonable to me. I will say that this doesn't solve all get+set >> > cases. For example, proxies. Hopefully, lazy objects can mostly bri= dge >> > this gap. >> > >> > Another case is lazy getters. >> > >> > ``` >> > class C { >> > public readonly int $magicNumber { >> > get =3D> expensiveComputation(); >> > } >> > } >> > ``` >> > >> > This does not seem to work in the current implementation: >> > >> >> Fatal error: Hooked virtual properties cannot be declared readonly >> > >> > I presume it would be possible to fix this, e.g. by using readonly = as >> > a marker to add a backing value to the property. I'm personally not >> > too fond of making the rules on which properties are backed more >> > complicated, as this is already a common cause for confusion. I also >> > fundamentally don't like that readonly changes whether get is calle= d. >> > Currently, if hooks are present, they are called. This adds more >> > special cases to an already complex feature. >> > >> > To me it seems the primary motivator for this RFC are readonly >> > classes, i.e. to prevent the addition of hooks from breaking readon= ly >> > classes. However, as lazy-getters are de-facto read-only, given they >> > are only writable from the extremely narrow scope of the hook itsel= f, >> > the modifier doesn't do much. Maybe an easier solution would be to >> > provide an opt-out of readonly. >>=20 >> Thanks, Ilija. You expressed my concerns as well. And yes, in pract= ice, readonly classes over-reaching is the main use case; if you're mark= ing individual properties readonly, then just don't mark the one that ha= s a hook on it (use aviz if needed) and there's no issue. =20 >>=20 >> Perhaps we're thinking about this the wrong way, though? So far we'v= e talked as though readonly makes the property write-once. But... what = if we think of it as applying to the field, aka the backing value? >>=20 >> So readonly doesn't limit calling the get hook, or even the set hook,= multiple times. Only writing to the actual value in the object table. = That gives the exact same set of guarantees that a getX()/setX() method= would give. The methods can be called any number of times, but the sto= red value can only be written once. >>=20 >> That would allow conditional set hooks, conditional gets, caching get= s (like we already have with ??=3D), and so on. The mental model is sim= ple and easy to explain/document. The behavior is the same as with meth= ods. But the identity of the stored value would be consistent. >>=20 >> It would not guarantee $foo->bar =3D=3D=3D $foo->bar in all cases (th= ough that would likely hold in the 99% case in practice), but then, $foo= ->getBar() =3D=3D=3D $foo->getBar() has never been guaranteed either. >>=20 >> Would that way of looking at it be acceptable to folks? >=20 > It does to me: readonly applies to the backed property, then hooks add= behavior as see fit. This is especially useful to intercept accesses to= said properties. Without readonly hooks, designing an abstract API that= uses readonly properties is a risky decision since it blocks any (futur= e) implementation that needs this interception capability. As a forward-= thinking author, one currently has two choices: not using readonly prope= rties in abstract APIs, or falling back to using getter/setters. That's = a design failure for hooks IMHO. I'm glad this RFC exists to fill this g= ap. >=20 > Nicolas To add to this, as I just mentioned on the Records thread, it would be g= ood to get hooks on readonly objects. With the new clone(), there is no = way to rely on validation in constructors. The most robust validation in= 8.5 can only be done via set/get hooks, but these hooks are not availab= le on readonly classes. This means that it is remarkably easy to "break"= objects that do constructor validation + use public(set) -- or use clon= e() in inherited objects instead of the parent constructor. In my experi= ence, readonly objects typically only do constructor validation (DRY). =E2=80=94 Rob --c6b5a68ff1e14308a9cbaa262c148972 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable
On Tue, Jul = 15, 2025, at 19:27, Nicolas Grekas wrote:

<= br>
Le lun. 14 juil. 2025 =C3=A0 = 15:41, Larry Garfield <larr= y@garfieldtech.com> a =C3=A9crit :
On Sun, Jul 13,= 2025, at 6:28 PM, Ilija Tovilo wrote:
> Hi Nick
>
> On Fri, Jul 11, 2025 at 6:31=E2=80=AFAM Nick <= php@nicksdot.dev> wrote:
>= >>
>>>
>>> To= not get this buried in individual answers to others:
>>= ;
>> I came up with two alternative implementations whi= ch cache the computed `get` hook value.
>> One leverage= s separate cache properties, the other writes directly to the backing st= ore.
>>
>> Links to the alternative br= anches can be found in the description of the original PR.
&g= t;> https://github.com/php/php-src/pull/18757<= /div>
>
> I am not a fan of the caching approach. = The implementation draft for
> this approach [^1] works by= storing the assigned value in the property
> slot, and re= placing it with the value returned from get one called for
&g= t; the first time. One of the issues here is that the backing value is
> observable without calling get. For example:
&= gt;
> ```
> class C {
>  =    public public(set) readonly string $prop {
>&= nbsp;        get =3D> strtoupper($this->prop);=
>     }
> }
> = $c =3D new C();
> $c->prop =3D 'foo';
> v= ar_dump(((array)$c)['prop']); // foo
> $c->prop;
<= div> > var_dump(((array)$c)['prop']); // FOO
> ```
>
> Here we can see that the underlying value ch= anges, despite the
> readonly declaration. This is especia= lly problematic for things like
> [un]serialize(), where c= alling serialize() before or after accessing
> the propert= y will change which underlying value is serialized. Even
>= worse, we don't actually know whether an unserialized property has
> already called the get hook.
>
>= ```
> class C {
>     public= public(set) readonly int $prop {
>      &n= bsp;  get =3D> $this->prop + 1;
>    =  }
> }
> $c =3D new C();
>= ; $c->prop =3D 1;
> $s1 =3D serialize($c);
&= gt; $c->prop;
> $s2 =3D serialize($c);
> = var_dump(unserialize($s1)->prop); // int(2)
> var_dump(= unserialize($s2)->prop); // int(3)
> ```
>= ;
> Currently, get is always called after unserialize(). T= here may be
> similar issues for __clone().
>= ;
> For readable and writable properties, the straight-for= ward solution is
> to move the logic to set.
&g= t;
> ```
> class C {
>  &= nbsp;  public public(set) readonly int $prop {
> = ;        set =3D> $value + 1;
>&nbs= p;    }
> }
> ```
><= /div>
> This is slightly differently, semantically, in that it e= xecutes any
> potential side-effects on write rather than = read, which seems
> reasonable. This also avoids the impli= cit mutation mentioned
> previously. At least in these cas= es, disallowing readonly + get seems
> reasonable to me. I= will say that this doesn't solve all get+set
> cases. For= example, proxies. Hopefully, lazy objects can mostly bridge
= > this gap.
>
> Another case is lazy gett= ers.
>
> ```
> class C {
=
>     public readonly int $magicNumber {
>         get =3D> expensiveComputati= on();
>     }
> }
= > ```
>
> This does not seem to work in t= he current implementation:
>
>> Fatal err= or: Hooked virtual properties cannot be declared readonly
>= ;
> I presume it would be possible to fix this, e.g. by us= ing readonly as
> a marker to add a backing value to the p= roperty. I'm personally not
> too fond of making the rules= on which properties are backed more
> complicated, as thi= s is already a common cause for confusion. I also
> fundam= entally don't like that readonly changes whether get is called.
> Currently, if hooks are present, they are called. This adds more=
> special cases to an already complex feature.
= >
> To me it seems the primary motivator for this RFC = are readonly
> classes, i.e. to prevent the addition of ho= oks from breaking readonly
> classes. However, as lazy-get= ters are de-facto read-only, given they
> are only writabl= e from the extremely narrow scope of the hook itself,
> th= e modifier doesn't do much. Maybe an easier solution would be to
> provide an opt-out of readonly.

Than= ks, Ilija.  You expressed my concerns as well.  And yes, in pr= actice, readonly classes over-reaching is the main use case; if you're m= arking individual properties readonly, then just don't mark the one that= has a hook on it (use aviz if needed) and there's no issue. 

Perhaps we're thinking about this the wrong way, = though?  So far we've talked as though readonly makes the property = write-once.  But... what if we think of it as applying to the field= , aka the backing value?

So readonly doesn't = limit calling the get hook, or even the set hook, multiple times.  = Only writing to the actual value in the object table.  That gives t= he exact same set of guarantees that a getX()/setX() method would give.&= nbsp; The methods can be called any number of times, but the stored valu= e can only be written once.

That would allow = conditional set hooks, conditional gets, caching gets (like we already h= ave with ??=3D), and so on.  The mental model is simple and easy to= explain/document.  The behavior is the same as with methods. = But the identity of the stored value would be consistent.
It would not guarantee $foo->bar =3D=3D=3D $foo->bar= in all cases (though that would likely hold in the 99% case in practice= ), but then, $foo->getBar() =3D=3D=3D $foo->getBar() has never bee= n guaranteed either.

Would that way of lookin= g at it be acceptable to folks?

It= does to me: readonly applies to the backed property, then hooks add beh= avior as see fit. This is especially useful to intercept accesses to sai= d properties. Without readonly hooks, designing an abstract API that use= s readonly properties is a risky decision since it blocks any (future) i= mplementation that needs this interception capability. As a forward-thin= king author, one currently has two choices: not using readonly propertie= s in abstract APIs, or falling back to using getter/setters. That's a de= sign failure for hooks IMHO. I'm glad this RFC exists to fill this = gap.

Nicolas

To add to this, as I just mentioned on the Records threa= d, it would be good to get hooks on readonly objects. With the new clone= (), there is no way to rely on validation in constructors. The most robu= st validation in 8.5 can only be done via set/get hooks, but these hooks= are not available on readonly classes. This means that it is remarkably= easy to "break" objects that do constructor validation + use public(set= ) -- or use clone() in inherited objects instead of the parent construct= or. In my experience, readonly objects typically only do constructor val= idation (DRY).

=E2=80=94 Ro= b
--c6b5a68ff1e14308a9cbaa262c148972--