Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128096 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 0AD131A00BC for ; Thu, 17 Jul 2025 13:21:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752758394; bh=O1J5sHCZz4Dr/JyevA3OUwqT47/rWlKiVPWAKmChFs8=; h=Date:From:To:Cc:In-Reply-To:References:Subject:From; b=YRoSyliIF5mt0hRparvTxcNJtsM/UdmgcvqY+b64C8gIlFYPDPmDSxI7/Sh4iW4O6 3krOdScvDg74cwHs23JBlOGGTL1RrvFqwUsIj7gd5ZsPpOtk8Z9sjPIyO6YJdqhISo dyS0cnlfFcWav3t/LLqZEk5DFav6/to6GMaPz1xNr2tKt6ymlOL3akLh/EW5EtdyTo e3Q3fq5/U15t0iBhYSibTTc1gqoewvGUmUDkIU6i05j5EFcc/gy85iPw97vAm9Sptk 3Bx931Dqwg2A2zhuvEJHfYSTpQSgU5uOvLvzk0sXNKjzzHWX7pEoUSYiVkiJ1+O8rx CEsruNzTJ+0LA== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 452091805EC for ; Thu, 17 Jul 2025 13:19: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.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 fhigh-b7-smtp.messagingengine.com (fhigh-b7-smtp.messagingengine.com [202.12.124.158]) (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 ; Thu, 17 Jul 2025 13:19:52 +0000 (UTC) Received: from phl-compute-05.internal (phl-compute-05.phl.internal [10.202.2.45]) by mailfhigh.stl.internal (Postfix) with ESMTP id 66DDA7A01A3; Thu, 17 Jul 2025 09:21:39 -0400 (EDT) Received: from phl-imap-05 ([10.202.2.95]) by phl-compute-05.internal (MEProxy); Thu, 17 Jul 2025 09:21:39 -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=1752758499; x= 1752844899; bh=bCbNol7IZLdHylWUieQNdMH55b/EgYgwQrZLdYHH9kM=; b=Y KXr8JkJB71PXjV78dC7c+oceeRVPJMRlTu18wdquwsjaeTBVnZ26bRX/HFAUsldy EmUKeY8jdcpAw2lafhOroTsbwOlHmCJVTfvw2lDXfcFtPlNywikN5U7dpmg4LTnf 2XOFpVfQMYfqcnu55cKUlQCUY80hmRWuiRf3sG+iKuhh7+CM/7yTGQWD5BfP6v71 T8DDimYsVUhryyU6/jETQ0G2JxDaxwXuYLEkjB9IgX2oiKM+RbXisl3kO6zel34o zH9sseX8ttJ29nTOU5oGCwtRF+zkxk+Sil5RmgXTy6X8SoQfz2JwXAwUV+aFpB/O IpmQc9TLh5I4ZMs5RNPlA== 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= 1752758499; x=1752844899; bh=bCbNol7IZLdHylWUieQNdMH55b/EgYgwQrZ LdYHH9kM=; b=FVHAnI0aKR32lDyshw1dNOLMkRX7D4qpLQ1pab36XYv3zjuP1yZ caB6X2xEyaHv7V5kBGxapjPIHJcZP8RgEFI3zB9OAYLplSDqJ98G03HKPH8pStPi zVLA3EvhuxesTXGi+Y15VA8id2WGk2T/vYwFAVvUE5z+FYkH9JOq9e5BCGALlEhb 8f1LQ8F/C75T/EeSIULhTS3+wXhSUUQwvQlGS77nWL7hovVPTp+scNe9y6bmmcEh yunkq2Gra09Akce4j7h0qFuV+KIpxBf6gHg2HyT0zF0tUtOruhhy7GUDojcuWLyb vGSpaKAM3hBezeXHDbN31hJJTsv/aozyYGg== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdefgdeitdeilecutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpuffrtefokffrpgfnqfghnecuuegr ihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmdenucfjug hrpefoggffhffvvefkjghfufgtsegrtderreertdejnecuhfhrohhmpedftfhosgcunfgr nhguvghrshdfuceorhhosgessghothhtlhgvugdrtghouggvsheqnecuggftrfgrthhtvg hrnhepteefffegvdduleegkedvuedvhfeifffggfdvudejieektdeltdfgkeevfeeggfef necuffhomhgrihhnpehphhhprdhnvghtpdhgihhthhhusgdrtghomhenucevlhhushhtvg hrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehrohgssegsohhtthhlvggu rdgtohguvghspdhnsggprhgtphhtthhopeegpdhmohguvgepshhmthhpohhuthdprhgtph htthhopehlrghrrhihsehgrghrfhhivghlughtvggthhdrtghomhdprhgtphhtthhopegv rhhitghtnhhorhhrihhssehgmhgrihhlrdgtohhmpdhrtghpthhtohepnhhitgholhgrsh drghhrvghkrghsodhphhhpsehgmhgrihhlrdgtohhmpdhrtghpthhtohepihhnthgvrhhn rghlsheslhhishhtshdrphhhphdrnhgvth X-ME-Proxy: Feedback-ID: ifab94697:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 6064C182007A; Thu, 17 Jul 2025 09:21:38 -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 15:21:16 +0200 To: erictnorris@gmail.com Cc: "Nicolas Grekas" , "Larry Garfield" , "php internals" Message-ID: 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> <4dee212b-55f9-4b7b-99c4-9fac4bc149cc@app.fastmail.com> Subject: Re: [PHP-DEV] [RFC] Readonly property hooks Content-Type: multipart/alternative; boundary=488f9f35363f466cba01f6ca1b936a1e From: rob@bottled.codes ("Rob Landers") --488f9f35363f466cba01f6ca1b936a1e Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable On Thu, Jul 17, 2025, at 15:10, Eric Norris wrote: > On Thu, Jul 17, 2025 at 3:31=E2=80=AFAM Rob Landers wrote: > > > > On Tue, Jul 15, 2025, at 19:27, Nicolas Grekas wrote: > > > > > > > > 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 wr= ote: > > >> > > >>> 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 co= mputed `get` hook value. > > >> One leverages separate cache properties, the other writes directl= y 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 f= or > > > this approach [^1] works by storing the assigned value in the prop= erty > > > 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 li= ke > > > [un]serialize(), where calling serialize() before or after accessi= ng > > > 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 solutio= n 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 se= ems > > > reasonable to me. I will say that this doesn't solve all get+set > > > cases. For example, proxies. Hopefully, lazy objects can mostly br= idge > > > 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 al= so > > > fundamentally don't like that readonly changes whether get is call= ed. > > > 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 reado= nly > > > classes. However, as lazy-getters are de-facto read-only, given th= ey > > > are only writable from the extremely narrow scope of the hook itse= lf, > > > the modifier doesn't do much. Maybe an easier solution would be to > > > provide an opt-out of readonly. > > > > Thanks, Ilija. You expressed my concerns as well. And yes, in prac= tice, readonly classes over-reaching is the main use case; if you're mar= king individual properties readonly, then just don't mark the one that h= as 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 the exact same set of guarantees that a getX()/setX() metho= d would give. The methods can be called any number of times, but the st= ored value can only be written once. > > > > That would allow conditional set hooks, conditional gets, caching ge= ts (like we already have with ??=3D), and so on. The mental model is si= mple and easy to explain/document. The behavior is the same as with met= hods. But the identity of the stored value would be consistent. > > > > It would not guarantee $foo->bar =3D=3D=3D $foo->bar in all cases (t= hough that would likely hold in the 99% case in practice), but then, $fo= o->getBar() =3D=3D=3D $foo->getBar() has never been guaranteed either. > > > > Would that way of looking at it be acceptable to folks? > > > > > > It does to me: readonly applies to the backed property, then hooks a= dd behavior as see fit. This is especially useful to intercept accesses = to said properties. Without readonly hooks, designing an abstract API th= at uses readonly properties is a risky decision since it blocks any (fut= ure) implementation that needs this interception capability. As a forwar= d-thinking author, one currently has two choices: not using readonly pro= perties 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= gap. > > > > Nicolas > > > > > > To add to this, as I just mentioned on the Records thread, 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 robust validatio= n in 8.5 can only be done via set/get hooks, but these hooks are not ava= ilable on readonly classes. This means that it is remarkably easy to "br= eak" objects that do constructor validation + use public(set) -- or use = clone() in inherited objects instead of the parent constructor. In my ex= perience, readonly objects typically only do constructor validation (DRY= ). >=20 > (shoot, double post, sorry Rob) >=20 > I'm not sure I follow - do you actually need both `set` and `get` > hooks for validation? I would think only `set` hooks would be > necessary, and I don't yet think I've seen an objection to `set` hooks > for `readonly`. >=20 It depends... for example, you might have an isValid property which comp= utes whether or not the object is valid, or in the case of lazy properti= es, detecting an invalid state there. You also might put validation in g= etters because you're building up the object over several lines, thus it= might only be in a partially valid state during construction: readonly class User { public public(set) $first_name; public public(set) $last_name; public $name { get =3D> implode(' ', [$this->first_name, $this->last_n= ame]) } } $user->first_name =3D "Rob" echo $user->name; // oops Or something. In this case, we can rely on the "uninitialized property" = exception to be raised if it isn't yet fully valid, but you might want t= o throw your own exception. =E2=80=94 Rob --488f9f35363f466cba01f6ca1b936a1e Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable


On Thu, Jul 17, 2025, at 15:10, Eric Norris wrote:
On Thu, Jul 17, 20= 25 at 3:31=E2=80=AFAM Rob Landers <rob@bottled.codes> wrote:
>
> On Tue= , Jul 15, 2025, at 19:27, Nicolas Grekas wrote:
>
>
>
> Le lun. 14 juil. 2025 =C3=A0 15:41, = Larry Garfield <larry@garfi= eldtech.com> a =C3=A9crit :
>
> On Sun,= Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote:
> > Hi Ni= ck
> >
> > On Fri, Jul 11, 2025 at 6:31=E2= =80=AFAM Nick <php@nicksdot.dev> wrote:
> >>&g= t;
&g= t; >>>
> >>> To not get this buried in in= dividual answers to others:
> >>
> >&= gt; I came up with two alternative implementations which cache the compu= ted `get` hook value.
> >> One leverages separate cac= he properties, the other writes directly to the backing store.
> >>
> >> Links to the alternative branches= can be found in the description of the original PR.
> >
>= > I am not a fan of the caching approach. The implementation draft f= or
> > this approach [^1] works by storing the assigned = value in the property
> > slot, and replacing it with th= e value returned from get one called for
> > the first t= ime. One of the issues here is that the backing value is
> = > observable without calling get. For example:
> >
> > ```
> > class C {
> >&= nbsp;    public public(set) readonly string $prop {
=
> >         get =3D&g= t; strtoupper($this->prop);
> >   &nbs= p; }
> > }
> > $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
> &= gt; the property will change which underlying value is serialized. Even<= /div>
> > worse, we don't actually know whether an unserialize= d property has
> > already called the get hook.
> >
> > ```
> > class C {
<= div>> >     public public(set) readonly int $p= rop {
> >        = ; get =3D> $this->prop + 1;
> >   &= nbsp; }
> > }
> > $c =3D new C();
<= div>> > $c->prop =3D 1;
> > $s1 =3D serialize($= c);
> > $c->prop;
> > $s2 =3D seriali= ze($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
> &= gt; similar issues for __clone().
> >
> >= ; For readable and writable properties, the straight-forward solution is=
> > to move the logic to set.
> >
=
> > ```
> > class C {
> >&nbs= p;    public public(set) readonly int $prop {
&= gt; >         set =3D> $va= lue + 1;
> >     }
> &g= t; }
> > ```
> >
> > Thi= s is slightly differently, semantically, in that it executes any
> > potential side-effects on write rather than read, which see= ms
> > reasonable. This also avoids the implicit mutatio= n mentioned
> > previously. At least in these cases, dis= allowing readonly + get seems
> > reasonable to me. I wi= ll say that this doesn't solve all get+set
> > cases. Fo= r example, proxies. Hopefully, lazy objects can mostly bridge
= > > this gap.
> >
> > Another case= is lazy getters.
> >
> > ```
= > > class C {
> >     public r= eadonly int $magicNumber {
> >    &n= bsp;    get =3D> expensiveComputation();
>= ; >     }
> > }
> &g= t; ```
> >
> > This does not seem to wor= k in the current implementation:
> >
> >= > Fatal error: Hooked virtual properties cannot be declared readonly<= /div>
> >
> > I presume it would be possible t= o fix this, e.g. by using readonly as
> > a marker to ad= d 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 confus= ion. I also
> > fundamentally don't like that readonly c= hanges whether get is called.
> > Currently, if hooks ar= e present, they are called. This adds more
> > special c= ases to an already complex feature.
> >
> &= gt; To me it seems the primary motivator for this RFC are readonly
=
> > classes, i.e. to prevent the addition of hooks from break= ing readonly
> > classes. However, as lazy-getters are d= e-facto read-only, given they
> > are only writable from= the extremely narrow scope of the hook itself,
> > the = modifier doesn't do much. Maybe an easier solution would be to
> > provide an opt-out of readonly.
>
>= Thanks, Ilija.  You expressed my concerns as well.  And yes, = in practice, readonly classes over-reaching is the main use case; if you= 're marking 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.
<= div>>
> 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 fiel= d, aka the backing value?
>
> So readonly does= n't limit calling the get hook, or even the set hook, multiple times.&nb= sp; Only writing to the actual value in the object table.  That giv= es the exact same set of guarantees that a getX()/setX() method would gi= ve.  The methods can be called any number of times, but the stored = value can only be written once.
>
> That would= allow conditional set hooks, conditional gets, caching gets (like we al= ready have 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.
<= div>>
> It would not guarantee $foo->bar =3D=3D=3D $f= oo->bar in all cases (though that would likely hold in the 99% case i= n practice), but then, $foo->getBar() =3D=3D=3D $foo->getBar() has= never been guaranteed either.
>
> Would that = way of looking at it be acceptable to folks?
>
&g= t;
> It does to me: readonly applies to the backed property= , then hooks add behavior as see fit. This is especially useful to inter= cept accesses to said properties. Without readonly hooks, designing an a= bstract API that uses readonly properties is a risky decision since it b= locks any (future) implementation that needs this interception capabilit= y. As a forward-thinking author, one currently has two choices: not usin= g readonly properties in abstract APIs, or falling back to using getter/= setters. That's a design failure for hooks IMHO. I'm glad this RFC exist= s to fill this gap.
>
> Nicolas
>=
>
> To add to this, as I just mentioned on th= e Records thread, it would be good to get hooks on readonly objects. Wit= h 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 available on readonly classes. This means that i= t is remarkably easy to "break" objects that do constructor validation += use public(set) -- or use clone() in inherited objects instead of the p= arent constructor. In my experience, readonly objects typically only do = constructor validation (DRY).

(shoot, double po= st, sorry Rob)

I'm not sure I follow - do you a= ctually need both `set` and `get`
hooks for validation? I woul= d think only `set` hooks would be
necessary, and I don't yet t= hink I've seen an objection to `set` hooks
for `readonly`.


It depends... for exam= ple, you might have an isValid property which computes whether or not th= e object is valid, or in the case of lazy properties, detecting an inval= id state there. You also might put validation in getters because you're = building up the object over several lines, thus it might only be in a pa= rtially valid state during construction:

readon= ly class User {
  public public(set) $first_name;
  public public(set) $last_name;
  public $name {= get =3D> implode(' ', [$this->first_name, $this->last_name]) }=
}

$user->first_name =3D "Rob"
echo $user->name; // oops

Or somethin= g. In this case, we can rely on the "uninitialized property" exception t= o be raised if it isn't yet fully valid, but you might want to throw you= r own exception.

=E2=80=94 = Rob
--488f9f35363f466cba01f6ca1b936a1e--