Hi Rowan On Sat, Mar 16, 2024 at 9:32 AM Rowan Tommins [IMSoP] <imsop....@rwec.co.uk> wrote: > > On 16 March 2024 00:19:57 GMT, Larry Garfield <la...@garfieldtech.com> wrote: > > >Well, reading/writing from within a set/get hook is an obvious use case to > >support. We cannot do cached properties easily otherwise: > > > >public string $expensive { > > get => $this->expensive ??= $this->compute(); > > set { > > if (strlen($value) < 50) throw new Exception(); > > $this->expensive = $value; > > } > >} > > > To play devil's advocate, in an implementation with only virtual properties, > this is still perfectly possible, just one declaration longer: > > private string $_expensive; > public string $expensive { > get => $this->_expensive ??= $this->compute(); > set { > if (strlen($value) < 50) throw new Exception(); > $this->_expensive = $value; > } > } > > Note that in this version there is an unambiguous way to refer to the raw > value from anywhere else in the class, if you wanted a clearAll() method for > instance. > > I can't stress enough that this is where a lot of my thinking comes from: > that backed properties are really the special case, not the default. Anything > you can do with a backed property you can do with a virtual one, but the > opposite will never be true. > > > The minimum version of backed properties is basically just sugar for that - > the property is still essentially virtual, but the language declares the > backing property for you, leading to: > > public string $expensive { > get => $field ??= $this->compute(); > set { > if (strlen($value) < 50) throw new Exception(); > $field = $value; > } > } > > I realise now that this isn't actually how the current implementation works, > but again I wanted to illustrate where I'm coming from: that backed > properties are just a convenience, not a different type of property with its > own rules.
That's not really how we think about it. Our design decisions have been guided by a few factors: 1. The RFC intentionally makes plain properties and properties with hooks as fully compatible as possible. A subclass can override a plain property by adding hooks to it. Many other languages only allow doing so if the parent property already has generated accessors (`{ get; set; }`). For many of them, switching from a plain property to one with accessors is actually an ABI break. One requires generating assembly/IR instructions that access a field in some structure, the other one is a method call. This is not relevant in our case. In most languages, a consequence of `{ get; set; }` is that such properties cannot be passed by reference. This part _is_ relevant to PHP, because PHP makes heavy use of explicit by-reference passing for arrays, but not much else. However, as outlined in the RFC, arrays are not a good use-case for hooks to begin with. So instead of fragmenting the entirety of all PHP code bases into plain and `{ get; set; }` properties where it doesn't actually make a semantic difference, and then not even using them when it would matter (arrays), we have decided to avoid generated hooks altogether. The approach of making plain and hooked properties compatible also immediately means that a property can have both a "backing value" (inherited from the parent property) and hooks (from the child property). This goes against your model that backed properties are really just two properties, one for the backing value and a virtual one for the hooks. Our approach has the nice side effect of properties only containing hooks when they actually do something. We don't need to deal with optimizations like "the hook is auto-generated, revert to accessing the property directly to make it faster", or even just having the generated hook taking up unnecessary memory. You can think of our properties this way: ```php class Property { public ?Data $storage; public ?callable $getHook; public ?callable $setHook; public function get() { if ($hook = $this->getHook) { return $hook(); } else if ($storage) { return $storage->get(); } else { throw new Error('Property is write-only'); } } public function set($value) { if ($hook = $this->setHook) { $hook($value); } else if ($storage) { $storage->set($value); } else { throw new Error('Property is read-only'); } } } ``` Properties can inherit both storage and hooks from their parent. Hopefully, that helps with the mental model. Of course, in reality it is a bit more complicated due to guards and references. 2. Although you say backed properties are just syntactic, they really are not. For example, renaming a public property, making it private and replacing it with a new passthrough virtual property breaks serialization, as serialization works on the object's raw values. On the other hand, adding a hook to an existing property doesn't influence its backing value, so there is no impact on serialization. > > Being the same also makes the language more predictable, which is also a > > design goal for this RFC. (Hence why "this is the same logic as > > methods/__get/other very similar thing" is mentioned several times in the > > RFC. Consistency in expectations is generally a good thing.) > > I can only speak for myself, but my expectations were based on: > > a) How __get and __set are used in practice. That generally involves reading > and writing a private property, of either the same or different name from the > public one; and that private property is visible everywhere equally, no > special handling based on the call stack. > > b) What happens if you accidentally cause infinite recursion in a normal > function or method, which is that the language eventually hits a stack depth > limit and throws an error. > > So the assertion that the proposal was consistent with expectations surprised > me. It feels to me like something that will seem surprising to people when > they first encounter it, but useful once they understand the implications. Guards are used for dynamic property creation within `__get`/`__set`: https://3v4l.org/6u3SR#v8.3.4 When `__get` or `__set` are called, the object remembers that this property is being accessed via magic method. When you're already inside this magic method, another call will not be triggered, thus falling back to accessing the actual property of the object. In this case, this means adding a dynamic property. Dynamic properties are not particularly relevant today. The point was not to show how similar these two cases are, but to explain that there's an existing mechanism in place that works very well for hooks. We may invent some new mechanism to access the backing value, like `field = 'value'`, but for what reason? This would only make sense if the syntax we use is useful for something else. However, given that without guards it just leads to recursion, which I really can't see any use for, I don't see the point. Ilija