Re: [RFC[ Property accessor hooks, take 2

From: Date: Tue, 12 Mar 2024 22:43:46 +0000
Subject: Re: [RFC[ Property accessor hooks, take 2
References: 1 2 3  Groups: php.internals 
Request: Send a blank email to internals+get-122628@lists.php.net to get a copy of this message
On Tue, Mar 12, 2024, at 10:09 PM, Rowan Tommins [IMSoP] wrote: >> if a get hook for property >> $foo calls method bar(), then inside >> that method $this->foo will refer to the raw property, both read >> and write. If bar() is called from somewhere other than the hook, >> reading from $this->foo will trigger the >> get hook. This behavior is identical to that already used by >> __get and __set today. > > > > I'm slightly confused by this. > > If there is an actual property called $foo, then __get and __set will > be called only when it is out of visibility, regardless of the call > stack - e.g. a private property will always trigger __get from public > scope, and always access it directly from private scope: > https://3v4l.org/R5Yos That seems differ from what's > proposed, where > even a private call to bar() would trigger the hook. > > The protection against recursion appears to only be relevant for > completely undefined properties. For __get, the direct access can never > do anything useful - there's nothing to access: > https://3v4l.org/2nDZS > For __set, it is at least possible for the non-recursive write to > succeed, but only in the niche case of creating a dynamic property: > https://3v4l.org/dpYOj I'm not sure that there's any > equivalent to this > scenario for property hooks, since they can never be undefined/dynamic. It's slightly different, yes. The point is that the special behavior of a hook is disabled if you are within the call stack of a hook, just like the special behavior of __get/__set is disabled if you are within the call stack of __get/__set. What happens when you hit an operation that would otherwise go into an infinite loop is a bit different, but the "disable to avoid an infinite loop" logic is the same. So maybe "is the same" rather than "is identical", but otherwise it's the same concept. >> There is one exception to the above: if a property is virtual, then there is no presumed >> connection between the get and set operations. [...] For that reason, >> &get by reference is allowed for virtual properties, regardless >> of whether or not there is a set hook. > > > I don't agree with this, and the example immediately following it > demonstrates the exact opposite: the &get and set hooks are both > proxying to the same backing value, and have all the same problems as > if the property was non-virtual. I would imagine a lot of real-life > virtual properties would be doing something similar: converting to/from > a different type, proxying to another object, etc. > > I think this exception is unnecessarily complicated: either trust users > to handle the implications of combining &get with set, or forbid it. The point is to give the user the option for full backwards compatibility when it makes sense. This requires jumping through some hoops, which is the point. This is essentially equivalent to creating a by-ref getter + a setter, exposing the underlying property. By creating a virtual property, we are "accepting" that the two are detached. While we could disallow this, we recognize that there may be valid use-cases that we'd like to enable. It also parallels __get/__set, where using &__get means you can write to something without going through __set. In practice I expect it virtual properties with both hooks to be very rare. Most virtual properties will, I expect, be lazy-computed get-only values. >> Additionally, &get hooks are allowed for arrays as >> well, provided there is no set hook. > > > I mentioned in a previous e-mail the possibility of using the &get hook > for array writes. Has this been considered? > > That is: > > $c->arr['beep'] = 'boop'; > > Would be equivalent to: > > $temp =& $c->arr; > $temp['beep'] = 'boop'; > unset($temp); > > Which would be valid if $arr had an &get hook defined. With the change to allow &get in the absence of set, I believe that would already work. cf: https://3v4l.org/3Gnti/rfc#vrfc.property-hooks >> A set hook on a typed property must declare a parameter >> type that is the same as or contravariant (wider) from the type of the property. > >> Once a property has both a get and >> set operation, however, it is no longer covariant or contravariant >> for further extension. > > > > How do these two rules interact? > > Could this: > > public string $foo { > get => $this->_foo; > set(string|Stringable $value) { > $this->_foo = (string)$value; > } > } > > be over-ridden by this, where the property's "main type" remains > invariant but its "settable type" is contravariant? > > public string $foo { > get => $this->_foo; > set(string|Stringable|SomethingElse $value) { > $this->_foo = $value instanceof SomethingElse ? > $value->asString() : (string)$value; > } > } That would be legal. >> ReflectionProperty has several new methods to work with hooks. > > > There should be some way to reliably determine the "settable type" of a > property. At the moment, I think you would have to do something like > this: > > $setHook = $property->getHook(PropertyHookType::Set); > $writeType = $setHook === null ? $property->getType() : > $setHook->getParameters()[0]->getType(); Hm. Good point here. We'll probably need to add another method to ReflectionProperty for that. Stay tuned. --Larry Garfield

« previous php.internals (#122628) next »