On Thu, Jul 17, 2025 at 3:31 AM Rob Landers <rob@bottled.codes> wrote:
>
> On Tue, Jul 15, 2025, at 19:27, Nicolas Grekas wrote:
>
>
>
> Le lun. 14 juil. 2025 à 15:41, Larry Garfield <la...@garfieldtech.com> a 
> écrit :
>
> On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote:
> > Hi Nick
> >
> > On Fri, Jul 11, 2025 at 6:31 AM Nick <p...@nicksdot.dev> wrote:
> >>
> >>> On 8. Jun 2025, at 11:16, Larry Garfield <la...@garfieldtech.com> 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 computed 
> >> `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 property
> > 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 => strtoupper($this->prop);
> >     }
> > }
> > $c = new C();
> > $c->prop = '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 => $this->prop + 1;
> >     }
> > }
> > $c = new C();
> > $c->prop = 1;
> > $s1 = serialize($c);
> > $c->prop;
> > $s2 = 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 => $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 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 getters.
> >
> > ```
> > class C {
> >     public readonly int $magicNumber {
> >         get => 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 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 hooks from breaking readonly
> > classes. However, as lazy-getters are de-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.
>
> 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() method would 
> give.  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 already have with ??=), 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 === $foo->bar in all cases (though that 
> would likely hold in the 99% case in practice), but then, $foo->getBar() === 
> $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 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 (future) 
> implementation that needs this interception capability. As a forward-thinking 
> author, one currently has two choices: not using 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 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 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 constructor. In my experience, readonly objects 
> typically only do constructor validation (DRY).

(shoot, double post, sorry Rob)

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`.

Reply via email to