Hi Juliette On 9-4-2024 16:03, Juliette Reinders Folmer wrote: > On 8-4-2024 23:39, Ilija Tovilo wrote: >> >> https://wiki.php.net/rfc/property-hooks >> > > I realize it is late in the discussion period to speak up, but for months > I've been trying to find the words to express my concerns in a polite and > constructive way and have failed.
First of all, thank you for taking the time to analyze the RFC and form your thoughts. I know how much time and effort it takes. Nonetheless, I hope you understand that receiving feedback of this sort literally minutes before a vote is planned is close to the worst way to go about it, both from a practical standpoint, but also RFC author and reviewer morale. Many of the points you raise have been there for many months to a year, and many have been raised and discussed many times over. I will try to address each, and give people some time to respond. > And not just one syntax, but five and the differences in the semantics of the > function syntaxes are significant. I think this is somewhat of a misrepresentation. For reference, you can find the grammar here. It's only about <50 lines. https://github.com/php/php-src/blob/bf390b47c7522fd4d130be26b8ee97520f985275/Zend/zend_language_parser.y#L1088-L1131 Notably, `get` and `set` hooks don't actually have a different grammar. Instead, hooks have names, optional parameter lists, and an optional short (`=>`) or long (`{}`) body. Whether all possible combinations are considered different syntax is open to interpretation. It's true that additional rules apply to each hook, e.g. whether they are allowed to declare a parameter list, and how the return value is used. I could see an argument being made for allowing an empty parameter list (`()`) for `get`, for consistency. Let us know if you have any other ideas to make them more congruent. In any case, I don't believe PHP_CodeSniffer would need to handle each combination separately. What would your optimal solution look like? I feel this discussion cannot progress unless we have more concrete discussion points. > * The implicit "set" parameter, which does not have a explicit parameter, > does not have parentheses and has the magically created $value variable. To be a bit more exact, the parameter list is simply inferred to `(<property type> $value)` so that the user does not need to spell it out. This is not akin to other magic variables we had previously, like `$http_response_header`. > TL;DR: this RFC tries to do too much in one go and introduces a huge amount > of cognitive complexity with all the exceptions and the differences in > behaviour between virtual and backed properties. This cognitive complexity is > so high that I expect that the feature will catch most developers out a lot > of the time. Many people still say that this RFC doesn't do enough because it doesn't support x/y/z. This is including you: > * The arrow function variant for `set` with all the above differences + the > differences inherent to arrow functions with the above mentioned exceptions + > the implicit assignment, which breaks the expected behaviour of arrow > functions by assigning the result of the expression instead of returning it > (totally understandable, but still a difference). > * Properties _may_ or _may not_ have a default value anymore, depending on > whether the hooks cause it to be a backed or a virtual property. > * Properties with hooks can _be_ readonly, but cannot be declared as readonly. > * Only available for object properties, static properties are excluded. > * Readonly properties which are not denoted as readonly, but still are due to > their virtual nature (get without access to $this->prop), which can only be > figured out by, again, studying the contents of the hook function. There are more below that I will be commenting on in more detail. I understand that, in a perfect world, each language feature would automatically work with every other. In reality, this is either not possible, or complex and time consuming. We opted for the middle ground, allowing things that were workable and omitting things that weren't, or required unrelated changes. If this is not acceptable, then I genuinely don't know how to approach non-trivial RFCs. > * Properties can now be declared as abstract, but only with explicit hook > requirements, otherwise the abstract keyword is not allowed. > * Properties can now be declared on interfaces, but only with explicit hook > requirements, otherwise they are not allowed. The semantics of plain interface properties are not clear. Intuitively, `public $prop;` looks equivalent to `public $prop { get; set; }`, but due to reference semantics it is not. It's not even equivalent to `{ &get; set; }`, because by-reference hooked properties still have some inherent limitations (https://wiki.php.net/rfc/property-hooks#assignment_by_reference). Again, this could be addressed, but only at the cost of complexity. > * Abstract/Interface properties don't comply with the same > covariance/contravariance rules as other properties Do you mean that they are not invariant as other properties? I think it's generally accepted that LSP rules should be as lax as possible, while being sound. Note that this rule isn't unique to read-/write-only abstract properties either, but also applies to read-/write-only virtual properties. ```php class P { public string|int $readonly { get => 'foo'; } } class C extends P { public int $readonly { get => 42; } } ``` > And last but not least, there is the ability to declare property hooks in > constructor property promotion ... where we can now basically have a function > declaration within the signature of a method declaration.... with all five > possible syntax types. The grammar used for hooks in constructor property promotion is the same as the one used for properties. Am I wrong to assume this shouldn't cause any additional work for PHP_CodeSniffer? > * `var` for property declarations is not supported according to the RFC. This is a misunderstanding between Larry and me. I personally never intended to deprecate `var` in this RFC. We've briefly discussed this and decided to treat `var` as normal, meaning as an alias of `public`. > * Disallows references to properties, but only when there is a set hook. > * Disallows indirect modification of a property, but only when there is a set > hook. The RFC spends much time explaining these limitations. If the RFC is ever accepted, in any form, then we must accept either of these approaches: 1. The engine places limitations on by-reference usage of properties when there is a `set` hook. 2. The user can trivially circumvent the `set` hook. This limitation is not unique to PHP either. > * Write-only (virtual) properties, which cannot be read and you need to study > the hook declaration in detail to figure out if a property is or is not > write-only. Indeed. But this has also been discussed in much detail. One proposed solution was explicitly marking properties as virtual. This comes with the downside of changing the property signature, making conversion of a backed parent property to virtual (and vice versa) a BC break. If you have a different solution in mind, please share. > Oh, and hang on, they *can* be read from a method in the same class as long > as that method is called from within the set hook, so now the method will > need to either do a backtrace or use Reflection to figure out whether it has > access to the property value. Right. We use the same mechanism as `__get`/`__set` uses to protect against recursion. In particular, when entering a magic method for a particular property, PHP remembers this using a "property guard". It's really just a marker in the object that sets the magic method type, along with the property name. When the same property is accessed again, the magic method or hook is skipped, and PHP behaves as if the magic method or hook wasn't there at all. For `__get`, `__set` and hooks of virtual properties, this means accessing a dynamic property. For backed properties, this means accessing its backing value. After discussing it, we've decided to make virtual properties slightly less surprising by throwing, instead of trying to create a dynamic property. This would be consistent with how virtual read-, and write-only properties are handled outside of hooks, when they are written to or read, respectively. > now why does that remind me of the magic __...() methods ? That's no secret. We have mentioned many times that we use the same mechanism for accessing backing value as `__get`/`__set` uses for accessing dynamic properties. > * Whether a type can be specified on the parameter on `set` depends on > whether the property is typed. You cannot declare `set(mixed $value)` for an > untyped property, even though it would effectively be compatible. This is > inconsistent with the behaviour for, for instance method overloads, where > this is acceptable: https://3v4l.org/hbCor/rfc#vrfc.property-hooks , though > it is consistent with the behaviour of property overloads, where this is not > acceptable: https://3v4l.org/seDWM (anyone up for an RFC to fix this > inconsistency ?) For child properties, it is not legal to add `mixed` to a previously untyped property. https://3v4l.org/mScQS#v8.3.5 I was honestly not aware of the case you outlined. Anyway, either approach we chose will be inconsistent with the other. > * Changes the meaning of `$this->property` read/write access for all methods > called from within a hook while executing the hook. What is the right solution to this in your opinion? Recursion surely isn't it. Throwing when accessing the backing value outside of its corresponding hook? I will investigate whether this is possible in an efficient way, and whether it's worth considering. > * Creates two different flavour of properties: "backed" properties and > "virtual" properties with significantly different behaviours, raising > cognitive complexity. > This includes, but is not limited to the fact that set hooks can be > bypassed on virtual properties via a get reference. Sure, but is there actually a solution to this? Making all properties backed means that serialization includes backing values that aren't used, and that the property supports useless reads/writes for properties that are only read or only written to. Only supporting virtual properties means that there's an inevitable BC break when converting a backed property to a virtual one, because the explicit backing property cannot share the same name as the virtual one, which will once again affect serialization. > * The range of different behaviours for various forms of serialization. Indeed, we carefully picked the behavior that should make sense for each case. Isn't that better than the user having to remember that they need to handle serialization/JSON themselves, depending on the case, which they also need to identify themselves? > [1] The RFC also states in "Interaction with isset() and unset()" that the > behaviour of property hooks is consistent with how `isset()` interacts with > `__get()` today. This is not true, as in: the behaviour as described for > isset with magic methods is different than what the RFC states it is: > https://3v4l.org/2Arkh/rfc#vrfc.property-hooks Right, this paragraph is a bit misleading. In essence, `isset` triggers both `__isset` and `__get` to 1. verify that the property even exists and 2. if it does, that it is non-null. For hooked properties, we don't need to verify the former, because the property is explicitly declared. So we only call `get` and make sure its value is non-null. We'll make sure to clarify this better. > Additionally, it proposes for isset() to throw an Error for virtual > properties without a get hook. This is surprising behaviour as isset() by its > nature is expected to *not* throw errors, but return false for whatever is > not set or inaccessible from the current context: https://3v4l.org/3OlgM Personally, I would rather know when checking `isset` on something that can never return `true`. But I admit that this case is not clear-cut. The throwing approach can be relaxed to return false without a BC break though, but not the opposite. > [2] PHP supports "multi-property declarations", but I see no mention of these > in the RFC, which makes it unclear if and if so, how property hooks would > work with multi-property declarations. > class Foo { > public string $bar, $baz, $bab; > } The implementation does not allow mixing multi-property declarations with hooks. I find them confusing (e.g. are `public int $foo, $bar;` two typed properties, or one typed and one untyped?). The same applies for hooks. We'll make sure to spell this out explicitly. > [3] If a `set` hook gets a named parameter, is the magic `$value` variable > still available or not ? I'd expect not, but it is unclear from the RFC. No. As mentioned above, on omission of the parameter list, it is simply inferred. There's no "truly" magic `$value` variable. > [4] Why should ReflectionProperty::getRawValue() throw an error for static > properties ? Instead of returning the value, identically to > ReflectionProperty::getValue() ? This is surprising to me and I see no reason > for it. Because it's equivalent to `getValue`, and indicates the user is doing something they don't fully understand. > As for the impact on static analysis tools.... I won't comment too much on this, because it's your area of expertise. Of course, I sympathize. If there's a solution that works well for us _and_ you, let me know. But it doesn't sound like some simple tweaks can significantly improve this problem for you. Here are the things we're changing: * `var` continues to be supported. * We will change accessing a virtual property with the same name from within a method called from a hook to throw, rather than create a dynamic property. * We will change accessing a backed property from within a method called from a hook to throw as well, assuming it can be done performantly. (We need to verify this.) To summarize: I understand your concerns and why you raise them. However, none of the complexity in this RFC is arbitrary. Most of it is inherent in the problem space, and the RFC’s approach is the result of extensive experimentation to determine possible solutions. While some superficial changes are possible, the actual complex parts (reference handling, inheritance, interfaces, virtual properties, etc.) are all necessary complexity, not accidental complexity. Absent a novel alternative, which after over a year of work and extensive discussions on this list and elsewhere we do not believe exists, we believe this RFC represents "the best hooks implementation that PHP can support." And that implementation deserves a final vote to determine if the internals community wants hooks at all or not. Ilija