[RFC[ Property accessor hooks, take 2

php.internals

Larry Garfield

2 years ago
Hello again, fine Internalians. After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. Baring any major changes, we plan to bring this to a vote in mid-March. https://wiki.php.net/rfc/property-hooks It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. Note the FAQ question at the end, which explains some design choices. There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort.
-- Larry Garfield larry@garfieldtech.com

Erick de Azevedo Lima

2 years ago
It's really nice!
>>> We are willing to do so if the consensus is that it would be
beneficial, but want to ask before putting in the effort. I really think that it's not necessary, as the it still hooks into the default behavior of the properties. Even though I don't have voting privileges, I'm rooting a lot for it. Thank you again for this amazing work!
-- Erick

Pierre

2 years ago
Le 21/02/2024 à 19:55, Larry Garfield a écrit :
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. > > However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort. >
Yes please ! Pass ! I don't have voting rights, but we need this. Cheers, Pierre R.

Robert Landers

2 years ago
On Thu, Feb 22, 2024 at 1:03 AM Pierre <pierre-php@processus.org> wrote:
> > Le 21/02/2024 à 19:55, Larry Garfield a écrit : > > Hello again, fine Internalians. > > > > After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. > > > > Baring any major changes, we plan to bring this to a vote in mid-March. > > > > https://wiki.php.net/rfc/property-hooks > > > > It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. > > > > Note the FAQ question at the end, which explains some design choices. > > > > There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. > > > > However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort. > > > Yes please ! Pass ! > > I don't have voting rights, but we need this. > > Cheers, > > Pierre R.
I apologize if this has already been covered:
> There are two shorthand notations supported, beyond the optional argument to set. > First, if a hook's body is a single expression, then the { } and return statement may be omitted and replaced with =>, just like with arrow functions.
Does => do any special auto-capturing of variables like arrow functions or is it just a shorthand? Also, is this a meaningful shorthand to the example a little further down: public string $phone { set = $this->sanitizePhone(...); } or do we always have to write it out? public string $phone { set => $field = $this->sanitizePhone($value); } Would __PROPERTY__ be set inside sanitizePhone() as well? You mention several ways values are displayed (whether or not they use the get-hook), but what does the default implementation of `__debugInfo()` look like now (or is that out of scope or a silly question?) For attributes, it would be nice to be able to target hooks specifically with attributes instead of also all methods (e.g., a Attribute::TARGET_GET_HOOK const). For example, if I were writing a serialization library, I may want to specify #[UseRawValue] only on getters to ensure that only the raw value is serialized instead of the getter (which may be specific to the application logic, or #[GetFromMethod] to tell the serialization library to get the value from a completely different method. It wouldn't make sense to target just any method with that attribute.

Matthew Weier O'Phinney

2 years ago
On Wed, Feb 21, 2024 at 12:57 PM Larry Garfield <larry@garfieldtech.com> wrote:
> After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could > think of. Properties involve dealing with both references and inheritance, > both of which have complex implications. We believe we’ve identified the > most logical handling for all cases, though. >
Once again in reading the proposal, the first thing I'm struck by are the magic "$field" and "$value" variables inside accessors. The first time they are used, they're used without explanation, and they're jarring. Additionally, once you start defining the behavior of accessors... you don't start with the basics, but instead jump into some of the more esoteric usage, which does nothing to help with the questions I have. So, first: - Start with the most basic, most expected usage for each of reading and writing properties. - I need a better argument for why the $field and $value variables exist. Saying they're macros doesn't help those not deep into internals. As a user, why do they exist? Honestly, it's not obvious at all that I can just set something to the variable "$field", and if I didn't know about the feature and stumbled across a class that used accessors, I'd be wondering what "$field" is, and how the property ever gets set. (And yes, I've read the FAQ. Saying it's used in another language doesn't automatically make it good design.) I know you say in the narrative that you _can_ use `$this->propertyName = ` to set the value, but you also say it's _not recommended_ (though you do not indicate _why_). To somebody coming primarily from userland who's been doing OOP in PHP for over 20 years, it's far more clear what's happening. Alternately, I'd argue that both the accessors should require an argument that represents the reference to the property: public string $username { set($field, string|Stringable $value) { $field = (string) $value; } get ($field) => strtolower($field); } The same is true for $value. I'd recommend only ever allowing the construct `set ($value) {}`, as it's immediately clear that `$value` is the argument and value being provided to the accessor. When it's implicit, it's easy to lose context. Second: you don't have examples of defining BOTH get and set OTHER than when using expressions for both accessors or a mix. I'm actually unclear what the syntax is when both are defined. Is there supposed to be a `;` terminating each? Or a `,`? Or just an empty line? Again, this is one of the more common scenarios. It needs to be covered early, and clearly. Third: the caveats around usage with arrays... give me pause. While I'm personally trying to not use arrays as much as possible, a lot of code I see or contribute to still does, and the fact that an array property that uses a write accessor doesn't allow the same level of access as a normal array property is something I see leading to a lot of confusion and errors. I don't have a solution, but I worry that this one thing alone could be enough to prevent the passage of the RFC. Fourth: the syntax around inheritance is not intuitive, as it does not work in the same way as the rest of the language. I'm talking about this: public int $x { get: 2 * parent::$x::get() } Why do we need to use the accessors here? Why wouldn't it just be `parent::$x`? I want to be clear: I really like the idea behind this feature, and overall, I appreciate the design. From a user perspective, though, the above are things that I found jarring as they vary quite a bit from our current language design.
> > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: > Originally, this RFC was called “property accessors,” which is the > terminology used by most languages. During early development, when we had > 4 accessors like Swift, we changed the name to “hooks” to better indicate > that one was “hooking into” the property lifecycle. However, later > refinement brought it back down to 2 operations, get and set. That makes > the “hooks” name less applicable, and inconsistent with what other > languages call it. > > However, changing it back at this point would be a non-small amount of > grunt work. There would be no functional changes from doing so, but it’s > lots of renaming things both in the PR and the RFC. We are willing to do so > if the consensus is that it would be beneficial, but want to ask before > putting in the effort. >
I personally would go with just "accessors".
-- Matthew Weier O'Phinney mweierophinney@gmail.com https://mwop.net/ he/him

Tim Düsterhus

2 years ago
Hi On 2/22/24 00:02, Matthew Weier O'Phinney wrote:
> Once again in reading the proposal, the first thing I'm struck by are the > magic "$field" and "$value" variables inside accessors. The first time they > are used, they're used without explanation, and they're jarring. > > […] > > Second: you don't have examples of defining BOTH get and set OTHER than > when using expressions for both accessors or a mix. I'm actually unclear > what the syntax is when both are defined. Is there supposed to be a `;` > terminating each? Or a `,`? Or just an empty line? Again, this is one of > the more common scenarios. It needs to be covered early, and clearly. >
On a similar topic with regard to syntax and shorthands, I'd like to quote myself from my previous year's email (Message-ID: 17d7983b-68b7-e273-a445-f8399c2510cc@bastelstu.be, https://externals.io/message/120213#120216):
> (5) I strongly dislike the doubly abbreviated form of public string $fullName => $this->first . " " . $this->last;. Having just the extra > '>' in there to distinguish it from a regular property feels non-obvious.
It's always possible to follow-up with syntax that allows for additional brevity, the inverse is not true and I believe for such a semantically complex feature, having clear and syntax syntax would be beneficial. Best regards Tim Düsterhus

Larry Garfield

2 years ago
On Wed, Feb 21, 2024, at 11:02 PM, Matthew Weier O'Phinney wrote:
> On Wed, Feb 21, 2024 at 12:57 PM Larry Garfield <larry@garfieldtech.com> wrote: >> After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. >> >> Baring any major changes, we plan to bring this to a vote in mid-March. >> >> https://wiki.php.net/rfc/property-hooks >> >> It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. > > Once again in reading the proposal, the first thing I'm struck by are > the magic "$field" and "$value" variables inside accessors. The first > time they are used, they're used without explanation, and they're > jarring. > > Additionally, once you start defining the behavior of accessors... you > don't start with the basics, but instead jump into some of the more > esoteric usage, which does nothing to help with the questions I have. > > So, first: > > - Start with the most basic, most expected usage for each of reading > and writing properties. > - I need a better argument for why the $field and $value variables > exist. Saying they're macros doesn't help those not deep into > internals. As a user, why do they exist?
For $field, it's not a requirement. It's mostly for copy-paste convenience. A number of people have struggled on this point so if the consensus is to leave out $field and just use $this->propName directly, we can accept that. They can be re-added if reusable hook packages are added in the future (as noted in Future Scope). For $value, it's to avoid boilerplate. For the majority case, you'll be just operating on an individual value trivially. Checking it's range, or uppercasing it, or whatever. Requiring the developer to provide a name explicitly is just extra work; it's much the same as how PHP doesn't require you to pass $this as the first argument to a method explicitly, the way Python and Rust do. It's just understood that $this exists, and once you learn that it's obvious. On the occasions where you do want to specify an alternate name for some reason, or more likely you're providing a wider type, you can. But in the typical case it would just be one more thing for the dev to have to type out. This is especially true in what I expect to be a common case, which is promoted constructor arguments with an extra validator set hook on them. It also introduces some ambiguity. If I specify only the name, does that mean I'm widening the type to mixed? Or just that I'm omitting the name? If specifying the name is rare, that's not really a big deal. If it's required in every case, it's a confusion point in every case. In the interest of transparency. for comparison: * Kotlin appears to require an argument name, but by convention recommends using "value". * Swift makes it optional, with a default name of "newValue". (Same logic as in the RFC.) * C# ... as far as I can tell, doesn't support a custom name at all; it's always called "value", implicitly.
> Second: you don't have examples of defining BOTH get and set OTHER than > when using expressions for both accessors or a mix. I'm actually > unclear what the syntax is when both are defined. Is there supposed to > be a `;` terminating each? Or a `,`? Or just an empty line? Again, > this is one of the more common scenarios. It needs to be covered early, > and clearly.
... huh. I thought we had one in there somewhere. I will add one, thanks. Though to clarify, there's no separator character. public string $foo { get { // ... } set { // ... } }
> Third: the caveats around usage with arrays... give me pause. While I'm > personally trying to not use arrays as much as possible, a lot of code > I see or contribute to still does, and the fact that an array property > that uses a write accessor doesn't allow the same level of access as a > normal array property is something I see leading to a lot of confusion > and errors. I don't have a solution, but I worry that this one thing > alone could be enough to prevent the passage of the RFC.
We completely agree that it's a suboptimal situation. But as explained, it is the way it is because it's not possible (as far as we can tell) to fully support hooks on array properties. If you can think of one, please share, because we'd love to make this part better. I don't like it either, but we haven't found a way around it. And that caveat doesn't seem like a good enough reason to not support hooks everywhere we actually can.
> Fourth: the syntax around inheritance is not intuitive, as it does not > work in the same way as the rest of the language. I'm talking about > this: > > public int $x { > get: 2 * parent::$x::get() > } > > Why do we need to use the accessors here? Why wouldn't it just be `parent::$x`?
Almost. Ilija spent some time looking into this, and it's possible with caveats. First, there's then no way to differentiate between "access parent hook" and "read the static property $x on the parent". Arguably that's not a common case, but it is a point of confusion. The larger issue is that parent::$x can't be just a stand-in for the backing value in all cases. While supporting that for the = operator is straightforward enough, it wouldn't give us access to ++, --, <=, and the dozen or so other operators that could conceivably apply. In theory those could all be implemented manually, but Ilija described that as "hundreds to thousands of lines of code" to do, which... is not time or code well spent. :-) Especially as this is a very edge-case situation to begin with. (In practice, I expect auto-generated ORM proxy code to be the primary user of accessing a parent property from a child hook. I can think of very few other cases where I'd want to use it.) So we have the choice between making $a = parent::$prop and parent::$prop = $a work, but *nothing else*, inexplicably (creating confusion) or the slightly longer syntax that wouldn't support those other operations anyway so there's no confusion. We feel the current approach is the better trade off, but if the consensus generally is for the shorter-but-inconsistent syntax, that can be changed.
> I want to be clear: I really like the idea behind this feature, and > overall, I appreciate the design. From a user perspective, though, the > above are things that I found jarring as they vary quite a bit from our > current language design. >> >> Note the FAQ question at the end, which explains some design choices. >> >> There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. >> >> However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort. > > I personally would go with just "accessors".
On Thu, Feb 22, 2024, at 12:06 AM, Deleu wrote:
> This is a long, huge and comprehensive work, congratz to the authors. > > It clearly shows that so much thought and work has been put into it > that it makes me cautious to even ask for further clarification. > >> Javascript have similar features via a different syntax, although that syntax would not be viable for PHP > > Why not?
> It feels quite a natural syntax for PHP and from someone oblivious to > the internal work, it appears to be a slight marginal change to the > existing RFC. Given the extensive work of this RFC, it seems pretty > obvious that this syntax will not work, I just don't know why.
I've added an FAQ section explaining why the Python/JS approach wouldn't really work. To be clear, Ilija and I spent 100+ hours doing research and design before we started implementation (back in mid-late 2022). We did seriously consider the JS-style syntax, but in the end we found it created more problems than it solves. For the type of language PHP is (explicit typed properties), doing it on the property itself is a much cleaner approach. On Thu, Feb 22, 2024, at 1:14 AM, Robert Landers wrote:
> I apologize if this has already been covered: > >> There are two shorthand notations supported, beyond the optional argument to set. >> First, if a hook's body is a single expression, then the { } and return statement may be omitted and replaced with =>, just like with arrow functions. > > Does => do any special auto-capturing of variables like arrow > functions or is it just a shorthand?
No, there is nothing to capture. Inside the hook body you have $this the same as any other method.
> Also, is this a meaningful > shorthand to the example a little further down: > > public string $phone { > set = $this->sanitizePhone(...); > } > > or do we always have to write it out? > > public string $phone { > set => $field = $this->sanitizePhone($value); > }
Currently no, the set is always void; you have to write the value yourself. See the FAQ section on the topic for a detailed explanation. However, I just had a long discussion with Ilija and there is one possibility we could consider: Use the return value only on the shorthand (arrow-function-like) syntax. So you could do either of these, which would be equivalent: set { $this->phone = $this->santizePhone($value); } set => $this->santizePhone($value); This would have the advantage of offering a return-to-set mechanism, as well as even shorter syntax in the simple case (and no question of $field vs $this->propName). But it would have the disadvantage of being potentially inconsistent between the short and long version. It also would mean the short version is incompatible with virtual properties; using a short-set would create a backing value, so it's non-virtual. But since "simple validation, maybe in a promoted constructor property" is likely to be one of the main use cases, it would simplify that case. Not sure if that's a trade up or not. Thoughts from the list?
> Would __PROPERTY__ be set inside sanitizePhone() as well?
No. Like __CLASS__, it is materialized at compile time and has no meaning outside of its intended scope.
> You mention several ways values are displayed (whether or not they use > the get-hook), but what does the default implementation of > `__debugInfo()` look like now (or is that out of scope or a silly > question?)
var_dump() shows the underlying backing value, bypassing get, since it's debugging the object state. If you implement __debugInfo(), that's a method like any other and you can do what you like, though you'll be reading through get hooks (just like using __serialize()).
> For attributes, it would be nice to be able to target hooks > specifically with attributes instead of also all methods (e.g., a > Attribute::TARGET_GET_HOOK const). For example, if I were writing a > serialization library, I may want to specify #[UseRawValue] only on > getters to ensure that only the raw value is serialized instead of the > getter (which may be specific to the application logic, or > #[GetFromMethod] to tell the serialization library to get the value > from a completely different method. It wouldn't make sense to target > just any method with that attribute.
This feels very niche, honestly. Those would naturally have to be sub-cases of TARGET_METHOD anyway, so method-targeted attributes would need to be supported regardless. That makes hook-specific-targeting an easy and non-breaking add-on for a future RFC if it turns out to be useful in practice. --Larry Garfield

Stephen Reay

2 years ago
> On 23 Feb 2024, at 06:56, Larry Garfield <larry@garfieldtech.com> wrote: > > On Wed, Feb 21, 2024, at 11:02 PM, Matthew Weier O'Phinney wrote: >> On Wed, Feb 21, 2024 at 12:57 PM Larry Garfield <larry@garfieldtech.com> wrote: >>> After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. >>> >>> Baring any major changes, we plan to bring this to a vote in mid-March. >>> >>> https://wiki.php.net/rfc/property-hooks >>> >>> It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. >> >> Once again in reading the proposal, the first thing I'm struck by are >> the magic "$field" and "$value" variables inside accessors. The first >> time they are used, they're used without explanation, and they're >> jarring. >> >> Additionally, once you start defining the behavior of accessors... you >> don't start with the basics, but instead jump into some of the more >> esoteric usage, which does nothing to help with the questions I have. >> >> So, first: >> >> - Start with the most basic, most expected usage for each of reading >> and writing properties. >> - I need a better argument for why the $field and $value variables >> exist. Saying they're macros doesn't help those not deep into >> internals. As a user, why do they exist? > > For $field, it's not a requirement. It's mostly for copy-paste convenience. A number of people have struggled on this point so if the consensus is to leave out $field and just use $this->propName directly, we can accept that. They can be re-added if reusable hook packages are added in the future (as noted in Future Scope). > > For $value, it's to avoid boilerplate. For the majority case, you'll be just operating on an individual value trivially. Checking it's range, or uppercasing it, or whatever. Requiring the developer to provide a name explicitly is just extra work; it's much the same as how PHP doesn't require you to pass $this as the first argument to a method explicitly, the way Python and Rust do. It's just understood that $this exists, and once you learn that it's obvious. > > On the occasions where you do want to specify an alternate name for some reason, or more likely you're providing a wider type, you can. But in the typical case it would just be one more thing for the dev to have to type out. This is especially true in what I expect to be a common case, which is promoted constructor arguments with an extra validator set hook on them. > > It also introduces some ambiguity. If I specify only the name, does that mean I'm widening the type to mixed? Or just that I'm omitting the name? If specifying the name is rare, that's not really a big deal. If it's required in every case, it's a confusion point in every case. > > In the interest of transparency. for comparison: > > * Kotlin appears to require an argument name, but by convention recommends using "value". > * Swift makes it optional, with a default name of "newValue". (Same logic as in the RFC.) > * C# ... as far as I can tell, doesn't support a custom name at all; it's always called "value", implicitly. > >> Second: you don't have examples of defining BOTH get and set OTHER than >> when using expressions for both accessors or a mix. I'm actually >> unclear what the syntax is when both are defined. Is there supposed to >> be a `;` terminating each? Or a `,`? Or just an empty line? Again, >> this is one of the more common scenarios. It needs to be covered early, >> and clearly. > > ... huh. I thought we had one in there somewhere. I will add one, thanks. Though to clarify, there's no separator character. > > public string $foo { > get { > // ... > } > set { > // ... > } > } > >> Third: the caveats around usage with arrays... give me pause. While I'm >> personally trying to not use arrays as much as possible, a lot of code >> I see or contribute to still does, and the fact that an array property >> that uses a write accessor doesn't allow the same level of access as a >> normal array property is something I see leading to a lot of confusion >> and errors. I don't have a solution, but I worry that this one thing >> alone could be enough to prevent the passage of the RFC. > > We completely agree that it's a suboptimal situation. But as explained, it is the way it is because it's not possible (as far as we can tell) to fully support hooks on array properties. If you can think of one, please share, because we'd love to make this part better. I don't like it either, but we haven't found a way around it. And that caveat doesn't seem like a good enough reason to not support hooks everywhere we actually can. > > >> Fourth: the syntax around inheritance is not intuitive, as it does not >> work in the same way as the rest of the language. I'm talking about >> this: >> >> public int $x { >> get: 2 * parent::$x::get() >> } >> >> Why do we need to use the accessors here? Why wouldn't it just be `parent::$x`? > > Almost. Ilija spent some time looking into this, and it's possible with caveats. > > First, there's then no way to differentiate between "access parent hook" and "read the static property $x on the parent". Arguably that's not a common case, but it is a point of confusion. > > The larger issue is that parent::$x can't be just a stand-in for the backing value in all cases. While supporting that for the = operator is straightforward enough, it wouldn't give us access to ++, --, <=, and the dozen or so other operators that could conceivably apply. In theory those could all be implemented manually, but Ilija described that as "hundreds to thousands of lines of code" to do, which... is not time or code well spent. :-) Especially as this is a very edge-case situation to begin with. (In practice, I expect auto-generated ORM proxy code to be the primary user of accessing a parent property from a child hook. I can think of very few other cases where I'd want to use it.) > > So we have the choice between making $a = parent::$prop and parent::$prop = $a work, but *nothing else*, inexplicably (creating confusion) or the slightly longer syntax that wouldn't support those other operations anyway so there's no confusion. > > We feel the current approach is the better trade off, but if the consensus generally is for the shorter-but-inconsistent syntax, that can be changed. > >> I want to be clear: I really like the idea behind this feature, and >> overall, I appreciate the design. From a user perspective, though, the >> above are things that I found jarring as they vary quite a bit from our >> current language design. >>> >>> Note the FAQ question at the end, which explains some design choices. >>> >>> There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. >>> >>> However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort. >> >> I personally would go with just "accessors". > > On Thu, Feb 22, 2024, at 12:06 AM, Deleu wrote: > >> This is a long, huge and comprehensive work, congratz to the authors. >> >> It clearly shows that so much thought and work has been put into it >> that it makes me cautious to even ask for further clarification. >> >>> Javascript have similar features via a different syntax, although that syntax would not be viable for PHP >> >> Why not? > >> It feels quite a natural syntax for PHP and from someone oblivious to >> the internal work, it appears to be a slight marginal change to the >> existing RFC. Given the extensive work of this RFC, it seems pretty >> obvious that this syntax will not work, I just don't know why. > > I've added an FAQ section explaining why the Python/JS approach wouldn't really work. To be clear, Ilija and I spent 100+ hours doing research and design before we started implementation (back in mid-late 2022). We did seriously consider the JS-style syntax, but in the end we found it created more problems than it solves. For the type of language PHP is (explicit typed properties), doing it on the property itself is a much cleaner approach. > > On Thu, Feb 22, 2024, at 1:14 AM, Robert Landers wrote: > > >> I apologize if this has already been covered: >> >>> There are two shorthand notations supported, beyond the optional argument to set. >>> First, if a hook's body is a single expression, then the { } and return statement may be omitted and replaced with =>, just like with arrow functions. >> >> Does => do any special auto-capturing of variables like arrow >> functions or is it just a shorthand? > > No, there is nothing to capture. Inside the hook body you have $this the same as any other method. > >> Also, is this a meaningful >> shorthand to the example a little further down: >> >> public string $phone { >> set = $this->sanitizePhone(...); >> } >> >> or do we always have to write it out? >> >> public string $phone { >> set => $field = $this->sanitizePhone($value); >> } > > Currently no, the set is always void; you have to write the value yourself. See the FAQ section on the topic for a detailed explanation. > > However, I just had a long discussion with Ilija and there is one possibility we could consider: Use the return value only on the shorthand (arrow-function-like) syntax. > > So you could do either of these, which would be equivalent: > > set { > $this->phone = $this->santizePhone($value); > } > > set => $this->santizePhone($value); > > This would have the advantage of offering a return-to-set mechanism, as well as even shorter syntax in the simple case (and no question of $field vs $this->propName). But it would have the disadvantage of being potentially inconsistent between the short and long version. It also would mean the short version is incompatible with virtual properties; using a short-set would create a backing value, so it's non-virtual. But since "simple validation, maybe in a promoted constructor property" is likely to be one of the main use cases, it would simplify that case. > > Not sure if that's a trade up or not. Thoughts from the list? > >> Would __PROPERTY__ be set inside sanitizePhone() as well? > > No. Like __CLASS__, it is materialized at compile time and has no meaning outside of its intended scope. > >> You mention several ways values are displayed (whether or not they use >> the get-hook), but what does the default implementation of >> `__debugInfo()` look like now (or is that out of scope or a silly >> question?) > > var_dump() shows the underlying backing value, bypassing get, since it's debugging the object state. If you implement __debugInfo(), that's a method like any other and you can do what you like, though you'll be reading through get hooks (just like using __serialize()). > >> For attributes, it would be nice to be able to target hooks >> specifically with attributes instead of also all methods (e.g., a >> Attribute::TARGET_GET_HOOK const). For example, if I were writing a >> serialization library, I may want to specify #[UseRawValue] only on >> getters to ensure that only the raw value is serialized instead of the >> getter (which may be specific to the application logic, or >> #[GetFromMethod] to tell the serialization library to get the value >> from a completely different method. It wouldn't make sense to target >> just any method with that attribute. > > This feels very niche, honestly. Those would naturally have to be sub-cases of TARGET_METHOD anyway, so method-targeted attributes would need to be supported regardless. That makes hook-specific-targeting an easy and non-breaking add-on for a future RFC if it turns out to be useful in practice. > > --Larry Garfield
Hi Larry, It's good to see this idea still progressing. I have to agree with the other comment(s) that the implicit `$field`/`$value` variables seem odd to me. I understand the desire for brevity and the potential future scope of reused hooks, but this concept seems to fly in the face of many years of PHP reducing "magic" like this. To give one answer to your question about ambiguity if the `$value` parameter is required - I don't believe this is actually ambiguous, in the context of PHP: - method parameters in child classes don't implicitly 'inherit' the parent method parameter's type if they don't define one (they widen to mixed); - method return types have no implicit inheritance, they must declare a compatible return type; - typed class properties don't implicitly inherit the parent type when the type left off a child property - they must declare the same type. AFAIK there is no existing behaviour in PHP where omitting a type would mean "the type is implicitly inherited from X", it either means the same as mixed, or it's an error. The use of implicit variables like this also opens up a new potential for bugs, particularly in the `set` block, where the difference between setting a new local variable inside the method body and writing to the backing store is indistinguishable without knowing intent. Explicitly writing to `$this->propertyName` (or `$this->{__PROPERTY__}` in a hypothetical re-used/generated hook) is certainly longer, but it's also much less likely to suffer from a typo that is then missed by a test, especially given that dynamic properties are now deprecated, so writing to `$this->porpertyName` from a hook on `$propertyName` would likely trigger an error, while writing to `$feild` from a hook would produce no error. Ultimately so long as the syntax *allows* for being explicit (which it seems to, and it provides the magic constant allowing for future dynamic use in reused hooks) I'm generally positive about this. Given that these are both essentially deemed as "shortcuts" for convenience rather than actual differences in functionality, perhaps whether to include the implicit/magic `$field`/`$value` variables could be a secondary question(s) on the RFC? Also, a small nitpick: The link to your attributeutils repo in the examples page, is broken, and it would be nice to see a few examples showing the explicit version of the hooks. Cheers Stephen

Larry Garfield

2 years ago
On Fri, Feb 23, 2024, at 8:33 AM, Stephen Reay wrote:
>> On 23 Feb 2024, at 06:56, Larry Garfield <larry@garfieldtech.com> wrote:
> Hi Larry, > > It's good to see this idea still progressing. > > > I have to agree with the other comment(s) that the implicit > `$field`/`$value` variables seem odd to me. I understand the desire for > brevity and the potential future scope of reused hooks, but this > concept seems to fly in the face of many years of PHP reducing "magic" > like this.
The "magic" that PHP has been removing is mostly weird and illogical type casting. As noted, neither of these variables are any more "magic" than $this. However, since it seems no one likes $field, we have removed it from the RFC. Of note, to respond to your comment further down, $this->{__PROPERTY__} will not work. The virtual-property detection looks for the AST representation of $this->propName, and only that. Dynamic versions that turn into that at runtime cannot work, as it needs to be known at compile time. For $value, however, we feel strongly that having the default there is a necessary part of the ergonomic picture. In particular, given your comments here:
> To give one answer to your question about ambiguity if the `$value` > parameter is required - I don't believe this is actually ambiguous, in > the context of PHP: > - method parameters in child classes don't implicitly 'inherit' the > parent method parameter's type if they don't define one (they widen to > mixed); > - method return types have no implicit inheritance, they must declare a > compatible return type; > - typed class properties don't implicitly inherit the parent type when > the type left off a child property - they must declare the same type. > > AFAIK there is no existing behaviour in PHP where omitting a type would > mean "the type is implicitly inherited from X", it either means the > same as mixed, or it's an error.
That to me suggests that IF a custom variable name is provided, we should require also specifying the type. In which case, in the 95% case, if we require the full argument signature then the 95% case would need to double-specify the type, which is a hard-no from an ergonomic standpoint. Especially combined with the suggestion yesterday to allow return-to-set in the short-set version, that would mean comparing this: public string $phone { set(string $phone) => $this->phone = $this->sanitizePhone($phone); } To this: public string $phone { set => $this->sanitizePhone($value); } And to me, there's absolutely no contest. The latter has about 1/3 as many places for me to make a typo repeating the same information over again. Now imagine comparing the above in a property that's used with constructor promotion. public function __construct( public string $phone { set(string $phone) => $this->phone = $this->sanitizePhone($phone); } public string $phone { set => $this->sanitizePhone($value); } ) {} Again, it's absolutely no contest for me. I would detest writing the longer version every time. If PHP has been moving away from weird and inexplicable magic, it's also been moving away from needless boilerplate. (Constructor promotion being the best example, but not the only; types themselves are a factor here, as are arrow functions.) As the whole point of this RFC is to make writing common code easier, requiring redundant boilerplate for it to work is actively counter-productive. So what I'd suggest instead is "specify the full signature if you want a custom name OR wider type; or omit the param list entirely to get a same-type $value variable, which 99% of the time is all you need." We get that in return for documenting "$value is the default", which for someone who has already figured out $this, should be a very low effort to learn.
> Also, a small nitpick: The link to your attributeutils repo in the > examples page, is broken, and it would be nice to see a few examples > showing the explicit version of the hooks.
Link fixed, thanks. What do you mean explicit version of the hooks? --Larry Garfield

Frederik Bosch | Genkgo

2 years ago
On 23-02-2024 16:58, Larry Garfield wrote:
> On Fri, Feb 23, 2024, at 8:33 AM, Stephen Reay wrote: >>> On 23 Feb 2024, at 06:56, Larry Garfield<larry@garfieldtech.com> wrote: >> Hi Larry, >> >> It's good to see this idea still progressing. >> >> >> I have to agree with the other comment(s) that the implicit >> `$field`/`$value` variables seem odd to me. I understand the desire for >> brevity and the potential future scope of reused hooks, but this >> concept seems to fly in the face of many years of PHP reducing "magic" >> like this. > The "magic" that PHP has been removing is mostly weird and illogical type casting. As noted, neither of these variables are any more "magic" than $this. > > However, since it seems no one likes $field, we have removed it from the RFC. Of note, to respond to your comment further down, $this->{__PROPERTY__} will not work. The virtual-property detection looks for the AST representation of $this->propName, and only that. Dynamic versions that turn into that at runtime cannot work, as it needs to be known at compile time. > > For $value, however, we feel strongly that having the default there is a necessary part of the ergonomic picture. In particular, given your comments here: > >> To give one answer to your question about ambiguity if the `$value` >> parameter is required - I don't believe this is actually ambiguous, in >> the context of PHP: >> - method parameters in child classes don't implicitly 'inherit' the >> parent method parameter's type if they don't define one (they widen to >> mixed); >> - method return types have no implicit inheritance, they must declare a >> compatible return type; >> - typed class properties don't implicitly inherit the parent type when >> the type left off a child property - they must declare the same type. >> >> AFAIK there is no existing behaviour in PHP where omitting a type would >> mean "the type is implicitly inherited from X", it either means the >> same as mixed, or it's an error. > That to me suggests that IF a custom variable name is provided, we should require also specifying the type. In which case, in the 95% case, if we require the full argument signature then the 95% case would need to double-specify the type, which is a hard-no from an ergonomic standpoint. > > Especially combined with the suggestion yesterday to allow return-to-set in the short-set version, that would mean comparing this: > > public string $phone { > set(string $phone) => $this->phone = $this->sanitizePhone($phone); > } > > To this: > > public string $phone { > set => $this->sanitizePhone($value); > } > > And to me, there's absolutely no contest. The latter has about 1/3 as many places for me to make a typo repeating the same information over again. Now imagine comparing the above in a property that's used with constructor promotion. > > public function __construct( > public string $phone { set(string $phone) => $this->phone = $this->sanitizePhone($phone); } > public string $phone { set => $this->sanitizePhone($value); } > ) {} > > Again, it's absolutely no contest for me. I would detest writing the longer version every time. > > If PHP has been moving away from weird and inexplicable magic, it's also been moving away from needless boilerplate. (Constructor promotion being the best example, but not the only; types themselves are a factor here, as are arrow functions.) As the whole point of this RFC is to make writing common code easier, requiring redundant boilerplate for it to work is actively counter-productive. > > So what I'd suggest instead is "specify the full signature if you want a custom name OR wider type; or omit the param list entirely to get a same-type $value variable, which 99% of the time is all you need." We get that in return for documenting "$value is the default", which for someone who has already figured out $this, should be a very low effort to learn. > >> Also, a small nitpick: The link to your attributeutils repo in the >> examples page, is broken, and it would be nice to see a few examples >> showing the explicit version of the hooks. > Link fixed, thanks. What do you mean explicit version of the hooks? > > --Larry Garfield
Hi Larry, Great to see you pick up on this proposal again. Rather than "just use $this->propName directly", you might want to consider yield to commit the property value, as a solution to "to assign and confirm the property write while still allowing code to run after it". But then return would be also be a logical consequence. public string$propName { set($value) { yield $value; $this->doSomething(); yield $value; return $this->doSomethingElse(); // final commit, not required } } If I read your remarks on why not using return I do not get why this proposal wants to deviate from a normal function syntax. function test(string $name) {     return [$first, $last] = explode(' ', $name); } var_dump(test("Larry Garfield")); // returns ["Larry", "Garfield"] That's how it always has been, no? So in your example, short code abbreviated form would not work. One has to write a block. public string$fullName { set=> [$this->first, $this->last] = explode <http://www.php.net/explode>(' ', \ucfirst <http://www.php.net/ucfirst>($value)); // error, $fullName is a string, returning array } public string$fullName { set{ [$this->first, $this->last] = explode <http://www.php.net/explode>(' ', \ucfirst <http://www.php.net/ucfirst>($value)); // no error, not returning } } Hope this RFC will develop into something that will pass. Good luck! Regards, Frederik

Rowan Tommins [IMSoP]

2 years ago
On 26/02/2024 19:02, Frederik Bosch wrote:
> > That's how it always has been, no? So in your example, short code > abbreviated form would not work. One has to write a block. > > public string$fullName { > set=> [$this->first, $this->last] = explode <http://www.php.net/explode>(' ', \ucfirst <http://www.php.net/ucfirst>($value)); // error, $fullName is a string, returning array > } > > public string$fullName { > set{ > [$this->first, $this->last] = explode <http://www.php.net/explode>(' ', \ucfirst <http://www.php.net/ucfirst>($value)); // no error, not returning > } > }
I think the intention is that both the block and the arrow syntax would have any return value ignored, as happens with constructors, for example. Note that in PHP, there is actually no such thing as "a function not returning a value", even a "void" function actually returns null; so if the return value was treated as meaningful, your second example would give an error "cannot assign null to property of type string". However, as noted in a previous message, I agree that the short form meaning "the value returned is saved to the backing field" is both more expected and more useful. The "yield" idea is ... interesting. I think personally I find it a bit too magic, and too cryptic to be more readable than an explicit assignment. Opinions may vary, though. Regards,
-- Rowan Tommins [IMSoP]

Frederik Bosch | Genkgo

2 years ago
Hi Rowan, Thanks for clearing up that the return value will be ignored. I understand better why that is (void = null). I do like the updated RFC better than the one with the $field variable to write to. My yield suggestion was an idea derived from that earlier $field write proposal, but I wanted to share it anyhow. I do note that $this->propName might suggest that the backing value is accessible from other locations than only the property's own get/set methods, because of $this usage. I would rather have a $field in get referring to the current value, and a $value in set referring to the passed value. So with the full name example:     public string $fullName {         get ($field): string => $field;         set ($value): string {             [$this->first, $this->last] = explode(' ', $value);             return $value;         }     } I think it would make absolutely clear that the backing value is only accessible in the local get/set scope. Regarding returning void=null, this is something that IDE and static analyzers already pick-up as an error. I think being stricter on that in this RFC would actually make sense, and treat void not as null. So with a slightly different full name example:     public string $fullName {         get ($field): string => $this->first . ' ' . $this->last;         set ($value): void {             [$this->first, $this->last] = explode(' ', $value); // real void, no value returned         }     } And why yield is magic, I do not get that. The word and the expression actually expresses that something is, well, yielded. yield is a word that is reserved in the language that serves the purpose of the problem. I would use it. It is even explainable that the set function is treated as a generator function. Regards, Frederik On 26-02-2024 20:39, Rowan Tommins [IMSoP] wrote:
> On 26/02/2024 19:02, Frederik Bosch wrote: >> >> That's how it always has been, no? So in your example, short code >> abbreviated form would not work. One has to write a block. >> >>      public  string$fullName  {           set=> [$this->first,  >> $this->last]  =  explode <http://www.php.net/explode>(' ',  \ucfirst >> <http://www.php.net/ucfirst>($value));  // error, $fullName is a >> string, returning array >>      } >>        public  string$fullName  {           set{ >>              [$this->first,  $this->last]  =  explode >> <http://www.php.net/explode>(' ',  \ucfirst >> <http://www.php.net/ucfirst>($value));  // no error, not returning >>          } >>      } > > > I think the intention is that both the block and the arrow syntax > would have any return value ignored, as happens with constructors, for > example. Note that in PHP, there is actually no such thing as "a > function not returning a value", even a "void" function actually > returns null; so if the return value was treated as meaningful, your > second example would give an error "cannot assign null to property of > type string". > > However, as noted in a previous message, I agree that the short form > meaning "the value returned is saved to the backing field" is both > more expected and more useful. > > The "yield" idea is ... interesting. I think personally I find it a > bit too magic, and too cryptic to be more readable than an explicit > assignment. Opinions may vary, though. > > Regards, >
-- Frederik Bosch Partner Genkgo logo Mail: f.bosch@genkgo.nl <mailto:f.bosch@genkgo.nl> Web: support.genkgo.com <https://support.genkgo.com> Entrada 123 Amsterdam +31 20 244 1920 Genkgo B.V. staat geregistreerd bij de Kamer van Koophandel onder nummer 56501153

Rowan Tommins [IMSoP]

2 years ago
On 26/02/2024 20:21, Frederik Bosch wrote:
> I do note that $this->propName might suggest that the backing value is > accessible from other locations than only the property's own get/set > methods, because of $this usage.
Yes, I actually stumbled over that confusion when I was writing some of the examples in my lengthy e-mail in this thread. As I understand it, this would work: public string $foo {     get { $this->foo ??= 0; $this->foo++; return $this->foo; }     set { throw new Exception; } } Outside the hooks, trying to write to $this->foo would throw the exception, because it refers to the hooked property as a whole; but inside, the same name refers to something different, which isn't accessible anywhere else. Now that I've looked more at how Kotlin uses "field", I understand why it makes sense - it's not an alias for the property itself, but the way to access a "backing store" which has no other name. Using $this->foo as the name is tempting if you think of hooks as happening "on top of" the "real" property; but that would be a different feature, like Switft's "property observers" (willSet and didSet). What's really happening is that we're declaring two things at once, and giving them the same name; almost as if we'd written this: public string $foo {     get { static $_foo; $_foo ??= 0; $_foo++; return $_foo; }     set { throw new Exception; } } Kotlin's "field" is kind of the equivalent of that "static $_foo"
> Regarding returning void=null, this is something that IDE and static > analyzers already pick-up as an error. I think being stricter on that > in this RFC would actually make sense, and treat void not as null. >
What would happen if a setter contained both "return 42;" and "return;"? The latter is explicitly allowed in "void" functions, but is also allowed in a non-void function as meaning "return null;"
> And why yield is magic, I do not get that. The word and the expression > actually expresses that something is, well, yielded. >
But yielded to where? My mental model of "return to set" is that this: public string $name { set($value) { $x = something($value); return $x + 1; } } Is effectively: private function _name_set($value) { $x = something($value); return $x + 1; } } plus: $this->name = $this->_name_set($value); With "yield", I can't picture that simple translation; the "magic" is whatever translates the "yield" keyword into "$this->name =" I would file it with the type widening in the RFC: seems kind of cool, but probably isn't worth the added complexity. Regards,
-- Rowan Tommins [IMSoP]

Frederik Bosch | Genkgo

2 years ago
Hi Rowan, On 26-02-2024 23:46, Rowan Tommins [IMSoP] wrote:
> On 26/02/2024 20:21, Frederik Bosch wrote: >> I do note that $this->propName might suggest that the backing value >> is accessible from other locations than only the property's own >> get/set methods, because of $this usage. > > > Yes, I actually stumbled over that confusion when I was writing some > of the examples in my lengthy e-mail in this thread. As I understand > it, this would work: > > public string $foo { >     get { $this->foo ??= 0; $this->foo++; return $this->foo; } >     set { throw new Exception; } > } > > Outside the hooks, trying to write to $this->foo would throw the > exception, because it refers to the hooked property as a whole; but > inside, the same name refers to something different, which isn't > accessible anywhere else. > > Now that I've looked more at how Kotlin uses "field", I understand why > it makes sense - it's not an alias for the property itself, but the > way to access a "backing store" which has no other name. > > Using $this->foo as the name is tempting if you think of hooks as > happening "on top of" the "real" property; but that would be a > different feature, like Switft's "property observers" (willSet and > didSet). What's really happening is that we're declaring two things at > once, and giving them the same name; almost as if we'd written this: > > public string $foo { >     get { static $_foo; $_foo ??= 0; $_foo++; return $_foo; } >     set { throw new Exception; } > } > > Kotlin's "field" is kind of the equivalent of that "static $_foo" > >
And what happens in the following situation, how are multiple get calls working together? public string $fullName {     get => $this->first . ' ' . $this->last; // is this accessing the backed value, or is it accessing via get     set($value) => $this->fullName = $value; } public string $first {     get => explode(' ', $this->fullName)[0], // is this accessing the backed value, or is it accessing via get     set($value) => $value; } Isn't it weird that $this->propName gives different results from one get function, compared to the other. I would say $this->prop should always follow the same semantics as explained in the RFC (first __get/__set, then the accessor).
> >> Regarding returning void=null, this is something that IDE and static >> analyzers already pick-up as an error. I think being stricter on that >> in this RFC would actually make sense, and treat void not as null. >> > > What would happen if a setter contained both "return 42;" and > "return;"? The latter is explicitly allowed in "void" functions, but > is also allowed in a non-void function as meaning "return null;"
return 42; // returns (int)42 return; // early return, void, same as no return return null; // returns null
> > >> And why yield is magic, I do not get that. The word and the >> expression actually expresses that something is, well, yielded. >> > > But yielded to where? My mental model of "return to set" is that this: > > public string $name { set($value) { $x = something($value); return $x > + 1; } } > > Is effectively: > > private function _name_set($value) { $x = something($value); return $x > + 1; } } > plus: > $this->name = $this->_name_set($value); > > With "yield", I can't picture that simple translation; the "magic" is > whatever translates the "yield" keyword into "$this->name ="
You would picture it by explaining how it works from the source side. A set function that contains a yield turns the set function into a directly consumed generator. Considering the following: public string $first {     set($value) => {         yield 'First name';         yield 'Given name';         return 'My name';     } } the pseudo-code from the PHP source side would look as follows. $generator = setCall($class, 'first', $value); foreach ($generator as $value) {    writeProperty($class, 'first', $value); } if ($generator->hasReturn()) { writeProperty($class, 'first', $generator->getReturn()); }
> > I would file it with the type widening in the RFC: seems kind of cool, > but probably isn't worth the added complexity. > > > Regards, >
-- Frederik Bosch Partner Genkgo logo Mail: f.bosch@genkgo.nl <mailto:f.bosch@genkgo.nl> Web: support.genkgo.com <https://support.genkgo.com> Entrada 123 Amsterdam +31 20 244 1920 Genkgo B.V. staat geregistreerd bij de Kamer van Koophandel onder nummer 56501153

Larry Garfield

2 years ago
On Mon, Feb 26, 2024, at 7:39 PM, Rowan Tommins [IMSoP] wrote:
> On 26/02/2024 19:02, Frederik Bosch wrote: >> >> That's how it always has been, no? So in your example, short code >> abbreviated form would not work. One has to write a block. >> >> public string$fullName { >> set=> [$this->first, $this->last] = explode <http://www.php.net/explode>(' ', \ucfirst <http://www.php.net/ucfirst>($value)); // error, $fullName is a string, returning array >> } >> >> public string$fullName { >> set{ >> [$this->first, $this->last] = explode <http://www.php.net/explode>(' ', \ucfirst <http://www.php.net/ucfirst>($value)); // no error, not returning >> } >> } > > > I think the intention is that both the block and the arrow syntax would > have any return value ignored, as happens with constructors, for > example. Note that in PHP, there is actually no such thing as "a > function not returning a value", even a "void" function actually returns > null; so if the return value was treated as meaningful, your second > example would give an error "cannot assign null to property of type string".
This correct. Given a function test(): $ret = test('Larry Garfield'); There's no way to tell if $ret is a possibly-null value we should do something with, or null by side-effect. The RFC right now takes the stance of "it's null by side effect, always, so we never do anything with the return so it's consistent."
> However, as noted in a previous message, I agree that the short form > meaning "the value returned is saved to the backing field" is both more > expected and more useful.
You're the first person to comment on it, but I'm glad you agree. :-) I like it, but Ilija is still unsure about it.
> The "yield" idea is ... interesting. I think personally I find it a bit > too magic, and too cryptic to be more readable than an explicit > assignment. Opinions may vary, though. > > Regards,
Mixing in syntax only used for generators here seems like it's asking for trouble. It wouldn't actually be a coroutine, so using coroutine like syntax would just be confusing. It's confusing to me whether this implies the hook becomes a generator or not, which means it's likely to confuse a lot of other people. --Larry Garfield

Marc

2 years ago
Hi Larry, first of all thank you very much for this amazing work you two have been done :+1:. On 23.02.24 00:56, Larry Garfield wrote:
> On Wed, Feb 21, 2024, at 11:02 PM, Matthew Weier O'Phinney wrote: >> On Wed, Feb 21, 2024 at 12:57 PM Larry Garfield <larry@garfieldtech.com> wrote: >>> After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. >>> >>> Baring any major changes, we plan to bring this to a vote in mid-March. >>> >>> https://wiki.php.net/rfc/property-hooks >>> >>> It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. >> Once again in reading the proposal, the first thing I'm struck by are >> the magic "$field" and "$value" variables inside accessors. The first >> time they are used, they're used without explanation, and they're >> jarring. >> >> Additionally, once you start defining the behavior of accessors... you >> don't start with the basics, but instead jump into some of the more >> esoteric usage, which does nothing to help with the questions I have. >> >> So, first: >> >> - Start with the most basic, most expected usage for each of reading >> and writing properties. >> - I need a better argument for why the $field and $value variables >> exist. Saying they're macros doesn't help those not deep into >> internals. As a user, why do they exist? > For $field, it's not a requirement. It's mostly for copy-paste convenience. A number of people have struggled on this point so if the consensus is to leave out $field and just use $this->propName directly, we can accept that. They can be re-added if reusable hook packages are added in the future (as noted in Future Scope). >
I'm also feeling that introducing magic variables isn't the best design choice. I read your section about "Why do set hooks not return the value to set?" and I don't really agree. Let me explain ... 1. Virtual properties and technically all functions return a valid. I think it would me much less magic if property setters on virtual properties declare a void return type. This would make it very obvious that this is a virtual property even on having to read complex setters. 2. it disallows any action /after/ the assignment happens I actually think this would be a good think! An object property should only be set after everything has been done. If you want to do more it should either not be part of the setter or you should use a temporary variable what you have to do anyway to not leave your object in an incorrect state. Let's say we have the following setter: public string$name { set($value){ // do stuff $field = $value; // or $this->name = $value // do more stuff and (eventually) fail throw Exception(); } } try {     $object->name = 'test'; } finally {     $object->name; // what is name here ? } 3. ambiguity I actually feel that $field is ambiguous. What happens if you declare `set($field) {}` ? Does such construct let the engine set the property value immediately as the input value gets immediately assigned to the property via $field? Greetings, Marc

Larry Garfield

2 years ago
On Fri, Feb 23, 2024, at 5:35 PM, Marc Bennewitz wrote:
> Hi Larry, > > first of all thank you very much for this amazing work you two have been > done :+1:.
> I'm also feeling that introducing magic variables isn't the best design > choice. > I read your section about "Why do set hooks not return the value to > set?" and I don't really agree. > > Let me explain ... > > 1. Virtual properties and technically all functions return a valid. > > I think it would me much less magic if property setters on virtual > properties declare a void return type. > This would make it very obvious that this is a virtual property even on > having to read complex setters.
Making everyone type ": void" after every set hook, when we already know that's going to be the case, seems like a really bad developer experience. I talked with Ilija extensively about it, and there is no meaningful way to distinguish between "this method returned null" and "this method didn't return" from the call site in the engine. If we could, that would allow smarter detection of when it makes sense to use a return value. Hence my suggestion of allowing set-on-return only for the => form.
> 3. ambiguity > > I actually feel that $field is ambiguous. What happens if you declare > `set($field) {}` ? Does such construct let the engine set the property > value immediately as the input value gets immediately assigned to the > property via $field?
$field has already been removed. See previous email. --Larry Garfield

Rowan Tommins [IMSoP]

2 years ago
On Thu, 22 Feb 2024, at 23:56, Larry Garfield wrote:
> However, I just had a long discussion with Ilija and there is one > possibility we could consider: Use the return value only on the > shorthand (arrow-function-like) syntax. > > So you could do either of these, which would be equivalent: > > set { > $this->phone = $this->santizePhone($value); > } > > set => $this->santizePhone($value);
Regarding this point, I've realised that the current short-hand set syntax isn't actually any shorter: set { $this->phone = $this->santizePhone($value); } set => $this->phone = $this->santizePhone($value); It also feels weird to say both "the right-hand side must be a valid expression" and "the value of the expression is ignored". So I think making the short-hand be "expression to assign to the implicit backing field" makes a lot more sense. Regards,
-- Rowan Tommins [IMSoP]

Weirdan

2 years ago
Hi Larry and others On Fri, Feb 23, 2024 at 12:57 AM Larry Garfield <larry@garfieldtech.com> wrote:
> > > I've added an FAQ section explaining why the Python/JS approach wouldn't really work. To be clear, Ilija and I spent 100+ hours doing research and design before we started implementation (back in mid-late 2022). We did seriously consider the JS-style syntax, but in the end we found it created more problems than it solves. For the type of language PHP is (explicit typed properties), doing it on the property itself is a much cleaner approach.
The section you added [1] seems to focus on having both `public string $fistName` and `public function get/set firstName():string`, and how it's hard to keep types and visibility in sync. But I'm not sure if you considered making properties and accessors mutually exclusive. I mean the following: ```php class Person { public string $firstName; // compile time error, setter/getter defined public function __construct(private string $first, private string $last) {} public function get firstName(): string // compile time error, property defined { return $this->first . " " . $this->last; } public function set firstName(string $value): void // compile time error, property defined { [$this->first, $this->last] = explode(' ', $value); } } ``` This seems to address most of the counterpoints you listed, to some degree:
> What is the property type of the $firstName property?
Well, you propose to allow wider write-types yourself, so the question would apply there as well. But presumably, the property type is its read type - so whatever getter returns.
> but there's nothing inherent that forces, public string $firstName, get firstName()s return and set firstName()s parameter to be the same. Even if we could detect it as a compile error, it means one more thing that the developer has to keep track of and get right, in three different places.
With mutually exclusive accessors and properties it becomes just two places. And yes, accessor consistency would need to be checked at compile time. But the same can be said for the widening of write type you proposed.
> What about visibility? Do the get/set methods need to have the same visibility as the property?
When there's no property the question becomes moot.
> If not, does that become a way to do asymmetric visibility?
Yes.
> What about inconsistency between the method's visibility and the property visibility? How is that handled?
There's no inconsistency when there's no property. Accessor visibility can be different - allowing the asymmetric visibility you wanted to implement in your other RFC.
> How do you differentiate between virtual and non-virtual properties?
This one is hard to answer without asking another question: why would you need to? Does the requirement to know it stem from engine implementation details, or do you need as a person writing code in PHP?
> For non-virtual properties, if you need to triple-enter everything, we're back to constructors pre-promotion. Plus, the accessor methods could be anywhere in the class, potentially hundreds of lines away. That means just looking at the property declaration doesn't tell you what its logic is; the logic may be on line 960, which only makes keeping its type/visibility in sync with the property harder.
Forbidding property declaration reduces that to double. The rest is mostly stylistic and can be said about traditional (non-constructor-promoted) properties as well. Now this approach naturally has some open questions, foremost about inheritance. But we probably don't need to go into those details if you already explored this way and found some technical obstacles. If you did, it would probably make sense to list them in the FAQ section. [1] https://wiki.php.net/rfc/property-hooks#why_not_pythonjavascript-style_accessor_methods
-- Best regards, Bruce Weirdan mailto:weirdan@gmail.com

Weirdan

2 years ago
On Tue, Feb 27, 2024 at 6:22 PM Bruce Weirdan <weirdan@gmail.com> wrote:
> > Hi Larry and others > > On Fri, Feb 23, 2024 at 12:57 AM Larry Garfield <larry@garfieldtech.com> wrote: > > > > > > I've added an FAQ section explaining why the Python/JS approach wouldn't really work. To be clear, Ilija and I spent 100+ hours doing research and design before we started implementation (back in mid-late 2022). We did seriously consider the JS-style syntax, but in the end we found it created more problems than it solves. For the type of language PHP is (explicit typed properties), doing it on the property itself is a much cleaner approach. > > > The section you added [1] seems to focus on having both `public string > $fistName` and `public function get/set firstName():string`, and how > it's hard to keep types and visibility in sync. But I'm not sure if > you considered making properties and accessors mutually exclusive. I > mean the following: > > ```php > class Person > { > public string $firstName; // compile time error, setter/getter defined > > public function __construct(private string $first, private string $last) {} > > public function get firstName(): string // compile time > error, property defined > { > return $this->first . " " . $this->last; > } > > public function set firstName(string $value): void // compile > time error, property defined > { > [$this->first, $this->last] = explode(' ', $value); > } > } > ``` > > This seems to address most of the counterpoints you listed, to some degree: > > > What is the property type of the $firstName property? > > Well, you propose to allow wider write-types yourself, so the question > would apply there as well. But presumably, the property type is its > read type - so whatever getter returns. > > > but there's nothing inherent that forces, public string $firstName, get firstName()s return and set firstName()s parameter to be the same. Even if we could detect it as a compile error, it means one more thing that the developer has to keep track of and get right, in three different places. > > With mutually exclusive accessors and properties it becomes just two > places. And yes, accessor consistency would need to be checked at > compile time. But the same can be said for the widening of write type > you proposed. > > > What about visibility? Do the get/set methods need to have the same visibility as the property? > > When there's no property the question becomes moot. > > > If not, does that become a way to do asymmetric visibility? > > Yes. > > > What about inconsistency between the method's visibility and the property visibility? How is that handled? > > There's no inconsistency when there's no property. Accessor visibility > can be different - allowing the asymmetric visibility you wanted to > implement in your other RFC. > > > How do you differentiate between virtual and non-virtual properties? > > This one is hard to answer without asking another question: why would > you need to? Does the requirement to know it stem from engine > implementation details, or do you need as a person writing code in > PHP? > > > For non-virtual properties, if you need to triple-enter everything, we're back to constructors pre-promotion. Plus, the accessor methods could be anywhere in the class, potentially hundreds of lines away. That means just looking at the property declaration doesn't tell you what its logic is; the logic may be on line 960, which only makes keeping its type/visibility in sync with the property harder. > > Forbidding property declaration reduces that to double. The rest is > mostly stylistic and can be said about traditional > (non-constructor-promoted) properties as well. > > Now this approach naturally has some open questions, foremost about > inheritance. But we probably don't need to go into those details if > you already explored this way and found some technical obstacles. If > you did, it would probably make sense to list them in the FAQ section. > > [1] https://wiki.php.net/rfc/property-hooks#why_not_pythonjavascript-style_accessor_methods
Resending this since I've never got a reply and it's quite possible the message got lost due to mail list issues.
-- Best regards, Bruce Weirdan mailto:weirdan@gmail.com

Larry Garfield

2 years ago
On Mon, Mar 11, 2024, at 4:13 PM, Bruce Weirdan wrote:
> On Tue, Feb 27, 2024 at 6:22 PM Bruce Weirdan <weirdan@gmail.com> wrote: >> >> Hi Larry and others >> >> On Fri, Feb 23, 2024 at 12:57 AM Larry Garfield <larry@garfieldtech.com> wrote: >> > >> > >> > I've added an FAQ section explaining why the Python/JS approach wouldn't really work. To be clear, Ilija and I spent 100+ hours doing research and design before we started implementation (back in mid-late 2022). We did seriously consider the JS-style syntax, but in the end we found it created more problems than it solves. For the type of language PHP is (explicit typed properties), doing it on the property itself is a much cleaner approach. >> >> >> The section you added [1] seems to focus on having both `public string >> $fistName` and `public function get/set firstName():string`, and how >> it's hard to keep types and visibility in sync. But I'm not sure if >> you considered making properties and accessors mutually exclusive. I >> mean the following: >> >> ```php >> class Person >> { >> public string $firstName; // compile time error, setter/getter defined >> >> public function __construct(private string $first, private string $last) {} >> >> public function get firstName(): string // compile time >> error, property defined >> { >> return $this->first . " " . $this->last; >> } >> >> public function set firstName(string $value): void // compile >> time error, property defined >> { >> [$this->first, $this->last] = explode(' ', $value); >> } >> } >> ``` >> >> This seems to address most of the counterpoints you listed, to some degree: >> >> > What is the property type of the $firstName property? >> >> Well, you propose to allow wider write-types yourself, so the question >> would apply there as well. But presumably, the property type is its >> read type - so whatever getter returns. >> >> > but there's nothing inherent that forces, public string $firstName, get firstName()s return and set firstName()s parameter to be the same. Even if we could detect it as a compile error, it means one more thing that the developer has to keep track of and get right, in three different places. >> >> With mutually exclusive accessors and properties it becomes just two >> places. And yes, accessor consistency would need to be checked at >> compile time. But the same can be said for the widening of write type >> you proposed. >> >> > What about visibility? Do the get/set methods need to have the same visibility as the property? >> >> When there's no property the question becomes moot. >> >> > If not, does that become a way to do asymmetric visibility? >> >> Yes. >> >> > What about inconsistency between the method's visibility and the property visibility? How is that handled? >> >> There's no inconsistency when there's no property. Accessor visibility >> can be different - allowing the asymmetric visibility you wanted to >> implement in your other RFC. >> >> > How do you differentiate between virtual and non-virtual properties? >> >> This one is hard to answer without asking another question: why would >> you need to? Does the requirement to know it stem from engine >> implementation details, or do you need as a person writing code in >> PHP? >> >> > For non-virtual properties, if you need to triple-enter everything, we're back to constructors pre-promotion. Plus, the accessor methods could be anywhere in the class, potentially hundreds of lines away. That means just looking at the property declaration doesn't tell you what its logic is; the logic may be on line 960, which only makes keeping its type/visibility in sync with the property harder. >> >> Forbidding property declaration reduces that to double. The rest is >> mostly stylistic and can be said about traditional >> (non-constructor-promoted) properties as well. >> >> Now this approach naturally has some open questions, foremost about >> inheritance. But we probably don't need to go into those details if >> you already explored this way and found some technical obstacles. If >> you did, it would probably make sense to list them in the FAQ section. >> >> [1] https://wiki.php.net/rfc/property-hooks#why_not_pythonjavascript-style_accessor_methods > > > Resending this since I've never got a reply and it's quite possible > the message got lost due to mail list issues.
What you suggest might be possible, but it still runs into the problem of backed properties. Plain methods work fine for virtual properties, which makes sense for Python and JS as they don't have pre-defined properties. Once the language has predefined properties, a method-centric approach leaves a lot more to be manual, which makes it more work to achieve the same end. And then what if you want hooks on a property used in constructor promotion? Or if you cannot declare a property and similarly named hook-method, how do you add hooks to a plain property in a child class, something generated code like ORMs will most likely want to do? Even if it could be done, what would be the advantage? Other than "it looks like JS", I don't really see any benefit, just a lot of complexity we'd have to sort out and find the edge cases on all over again, and basically rewrite the whole RFC. Unless someone can show why the property-centric approach is fundamentally broken (and given that we have a successful implementation already I think that is unlikely), there's little reason to revisit the fundamentals of the RFC at this point. (And, mind you, the above discussion does not demonstrate that a method-centric approach is in fact equally feasible. At best, there are probably ways to make most things work, but without actually doing it there's no way to be sure it wouldn't run into a brick wall somewhere.) --Larry Garfield

Deleu

2 years ago
On Wed, Feb 21, 2024 at 3:58 PM Larry Garfield <larry@garfieldtech.com> wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could > think of. Properties involve dealing with both references and inheritance, > both of which have complex implications. We believe we’ve identified the > most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: > Originally, this RFC was called “property accessors,” which is the > terminology used by most languages. During early development, when we had > 4 accessors like Swift, we changed the name to “hooks” to better indicate > that one was “hooking into” the property lifecycle. However, later > refinement brought it back down to 2 operations, get and set. That makes > the “hooks” name less applicable, and inconsistent with what other > languages call it. > > However, changing it back at this point would be a non-small amount of > grunt work. There would be no functional changes from doing so, but it’s > lots of renaming things both in the PR and the RFC. We are willing to do so > if the consensus is that it would be beneficial, but want to ask before > putting in the effort. > > -- > Larry Garfield > larry@garfieldtech.com >
This is a long, huge and comprehensive work, congratz to the authors. It clearly shows that so much thought and work has been put into it that it makes me cautious to even ask for further clarification.
> Javascript have similar features via a different syntax, although that
syntax would not be viable for PHP Why not? ``` final class Foo { public string $bar; public function get bar(): string { // custom getter } public function set bar(string $value): void { // custom setter } } ``` It feels quite a natural syntax for PHP and from someone oblivious to the internal work, it appears to be a slight marginal change to the existing RFC. Given the extensive work of this RFC, it seems pretty obvious that this syntax will not work, I just don't know why.
-- Marco Deleu

Robert Landers

2 years ago
On Wed, Feb 21, 2024 at 7:59 PM Larry Garfield <larry@garfieldtech.com> wrote:
> > Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more polished property access hooks/interface properties RFC. It’s 99% unchanged from last summer; the PR is now essentially complete and more robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could think of. Properties involve dealing with both references and inheritance, both of which have complex implications. We believe we’ve identified the most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. > > However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort. > > -- > Larry Garfield > larry@garfieldtech.com
This is a reply to Marco (https://externals.io/message/122445#122449) for which I didn't actually receive the email but got an email from the list that I didn't receive the email -- seems like it would have been simpler and more correct to resend the email. That is a bit weird, but whatever.
> Given the extensive work of this RFC, it seems pretty obvious that > this syntax will not work, I just don't know why.
I feel like the syntax is natural, coming from other languages with this feature. However, I really do appreciate the example Marco gives as it feels very idiomatically PHP with all the getters/setters we are used to writing. It also means a pretty simple "just add a space and downcase" to switch from traditional methods to getters/setters to hooks, which could be really nice.

tag Knife

2 years ago
On Wed, 21 Feb 2024 at 18:56, Larry Garfield <larry@garfieldtech.com> wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could > think of. Properties involve dealing with both references and inheritance, > both of which have complex implications. We believe we’ve identified the > most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: > Originally, this RFC was called “property accessors,” which is the > terminology used by most languages. During early development, when we had > 4 accessors like Swift, we changed the name to “hooks” to better indicate > that one was “hooking into” the property lifecycle. However, later > refinement brought it back down to 2 operations, get and set. That makes > the “hooks” name less applicable, and inconsistent with what other > languages call it. > > However, changing it back at this point would be a non-small amount of > grunt work. There would be no functional changes from doing so, but it’s > lots of renaming things both in the PR and the RFC. We are willing to do so > if the consensus is that it would be beneficial, but want to ask before > putting in the effort. > > -- > Larry Garfield > larry@garfieldtech.com
I remember the previous RFC. cant remeber why it was decline, but i hope this one passes, I know the PHP community has been asking for native getter/setters instead of __get __set for a while, since 7.0 was released at least. A few things i was interested to get the idea around. Was it thought about for the set{} for it to return the value to set the property to instead implicitly setting its own field? eg ``` public string $name { set { return usfirst($value); } } ``` Where the value returned in the set is what the property will be set to?

Deleu

2 years ago
On Thu, Feb 22, 2024 at 8:25 AM tag Knife <fenniclog@gmail.com> wrote:
> A few things i was interested to get the idea around. > Was it thought about for the set{} for it to return the value to set the > property to > instead implicitly setting its own field? > > eg > > ``` > public string $name { > set { > return usfirst($value); > } > } > ``` > Where the value returned in the set is what the property will be set to? >
The answer to this question is best described on the RFC FAQ: https://wiki.php.net/rfc/property-hooks#why_do_set_hooks_not_return_the_value_to_set
-- Marco Deleu

Tim Düsterhus

2 years ago
Hi 1. I believe there is an error in the "Final Hooks" section:
> // But this is NOT allowed, because beforeSet is final in the parent.
I believe it should be s/beforeSet/set/. 2. In the same section this sentence is probably grammatically incorrect.
> Declaring hooks final on a property that is declared final is
redundant will throw an error. 3. Regarding the same sentence I also don't see why that should be disallowed, even if it is redundant. It's also legal to define 'final' functions within a 'final' class. Which also brings me to the question: Is defining 'final' properties and 'final' hooks within a 'final' class equally disallowed? 4. Since the RFC was last discussed, the #[\Override] RFC was accepted and implemented (https://wiki.php.net/rfc/marking_overriden_methods). A short section on the interaction with #[\Override] would probably be helpful. 5. Not sure if I've asked it before, but have you considered making the parameter for ReflectionProperty::getHook() an enum? On 2/21/24 19:55, Larry Garfield wrote:
> There’s one outstanding question, which is slightly painful to ask: Originally, this RFC was called “property accessors,” which is the terminology used by most languages. During early development, when we had 4 accessors like Swift, we changed the name to “hooks” to better indicate that one was “hooking into” the property lifecycle. However, later refinement brought it back down to 2 operations, get and set. That makes the “hooks” name less applicable, and inconsistent with what other languages call it. > > However, changing it back at this point would be a non-small amount of grunt work. There would be no functional changes from doing so, but it’s lots of renaming things both in the PR and the RFC. We are willing to do so if the consensus is that it would be beneficial, but want to ask before putting in the effort. >
Calling it hooks is fine and allows for future extension without renaming, should the need arise. Best regards Tim Düsterhus

Benjamin Eberlei

2 years ago
Hey, On Wed, Feb 21, 2024 at 7:58 PM Larry Garfield <larry@garfieldtech.com> wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could > think of. Properties involve dealing with both references and inheritance, > both of which have complex implications. We believe we’ve identified the > most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: > Originally, this RFC was called “property accessors,” which is the > terminology used by most languages. During early development, when we had > 4 accessors like Swift, we changed the name to “hooks” to better indicate > that one was “hooking into” the property lifecycle. However, later > refinement brought it back down to 2 operations, get and set. That makes > the “hooks” name less applicable, and inconsistent with what other > languages call it. > > However, changing it back at this point would be a non-small amount of > grunt work. There would be no functional changes from doing so, but it’s > lots of renaming things both in the PR and the RFC. We are willing to do so > if the consensus is that it would be beneficial, but want to ask before > putting in the effort.
thank you for this proposal. there are some points i'd like to make into this discussion: * Thank you for the removal of $field, it was non-idomatic from a PHP POV. * I would prefer that the short syntax $foo => null; be voted upon separately. Personally I think it could be confusing and is too close to a regular assignment for default value and I prefer not to have it and keep the rest of the RFC. * The magic of detecting if a property is virtual or backed is - like Rowan said - subtle. I would also prefer this to be managed via an explicit mechanism, by for example keywording the property as "virtual", instead of the implicit way with the parsing based detection. * As Doctrine project maintainer, we have had some troubles supporting read only properties for proxies. I would have hoped that with hooks I can overwrite a readonly property and "hook" into it instead by defining a getter, but "no setter". From my read of the RFC this would not be allowed and throws a compile time error. Could you maybe clarify why at least this special case is not possible? I didn't immediately get it from the RFC section on readonly as it only speaks about the problems in abstract. greetings Benjamin

Larry Garfield

2 years ago
On Tue, Feb 27, 2024, at 8:51 PM, Benjamin Außenhofer wrote:
> thank you for this proposal. there are some points i'd like to make > into this discussion: > > * Thank you for the removal of $field, it was non-idomatic from a PHP POV. > > * I would prefer that the short syntax $foo => null; be voted upon > separately. Personally I think it could be confusing and is too close > to a regular assignment for default value and I prefer not to have it > and keep the rest of the RFC.
See my long reply to Rowan just now, where I go into this.
> * The magic of detecting if a property is virtual or backed is - like > Rowan said - subtle. I would also prefer this to be managed via an > explicit mechanism, by for example keywording the property as > "virtual", instead of the implicit way with the parsing based detection.
See my long reply to Rowan just now, where I go into this.
> * As Doctrine project maintainer, we have had some troubles supporting > read only properties for proxies. I would have hoped that with hooks I > can overwrite a readonly property and "hook" into it instead by > defining a getter, but "no setter". From my read of the RFC this would > not be allowed and throws a compile time error. Could you maybe clarify > why at least this special case is not possible? I didn't immediately > get it from the RFC section on readonly as it only speaks about the > problems in abstract.
Once again, readonly properties were designed sloppily without consideration of how they would interact with other features. Full asymmetric visibility would have been much better suited to what you describe, but that was rejected. (If hooks pass, we are considering taking a second attempt at aviz. Haven't decided yet.) As for details, consider this technically legal (if ill-advised) code: public DateTimeImmutable $now { get { return new DateTimeImmutable(); } } If that property is non-readonly, then while that may be weird, it's not really unexpected. There's no guarantee a property won't change value from one access to the next. UNLESS that property is readonly. But if it's readonly, what the heck does the above code do? It would be returning a different value on every request, so it violates one of the constraints on readonly. The hook can do arbitrary logic, which means there's really no way for the engine to detect at compile time "wait, you're doing something dynamic." (That would be an extreme amount of work unless PHP was pure-by-default, which it is not.) For inheritance, this is the same reasoning for why a property cannot have the readonly marker removed in a child class. It breaks one of the expected constraints of the property, even if not the type per se. (So it's kinda a Liskov issue.) This is true regardless of whether the property is virtual or not. Virtual properties just have the extra complication of "when you try to set it, there's an if (is_uninitialized()) check in the engine. What does that even mean when there's no backing property??" There's one carve out that might be supportable, which is just a set hook on a readonly property. That MIGHT be able to work around readonly's flaws by making sure the set hook calls parent::$foo::set($value), to ensure the actual write happens in the parent (since readonly is private-set, not protected-set). Ilija tells me that could be tricky to do, however, so we'd rather punt on that for now. Follow-up RFCs (even this version) could flesh out that edge case alone without impacting the rest of the design. --Larry Garfield

Alexandru Pătrănescu

2 years ago
On Wed, Feb 21, 2024, 20:58 Larry Garfield <larry@garfieldtech.com> wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. >
I liked how the discussion and the RFC evolved so far and that we have less magic now. I didn't saw one aspect discussed, automatic detection if a property is virtual or not. Why not using explicit keyword for marking a property as virtual and not relying on the detection? Detection cannot be complete, and I can think if the property is used in another method called from the hook, we might end up with errors when extracting few lines of code as a method. Since in the reflection the "virtual" name is used, it would be natural and simple to have it explicitly as part of the property definition. Of course, a virtual property would require at least a hook type defined, otherwise there wouldn't be any point for its existence. Alex

Larry Garfield

2 years ago
Hi folks. Based on earlier discussions, we've made a number of changes to the RFC that should address some of the concerns people raised. We also had some very fruitful discussions off-list with several developers from the Foundation, which led to what we feel are some solid improvements. https://wiki.php.net/rfc/property-hooks Smaller changes: 1. We've added a PropertyHookType enum for use in reflection, which lets us get rid of a possible exception. 2. get_mangled_object_vars()'s behavior is now defined to be the same as an array, ie, skip hooks. 3. A final property with a final hook is no longer an error. It's just silently redundant, like for methods. 5. Made explict what happens with recursive hook calls and method calls from a hook. The behavior here is the same as for __get/__set. 6. Made support for #[\Override] explicit. 7. Added an FAQ regarding the property-centric approach rather than method-centric approach. 8. Clarified that the parent::$foo::get() syntax works on a parent property regardless of whether it has hooks. 9. Clarified that untyped properties are supported. (Though, it's 2024, please don't use untyped properties.) 10. Clarified that interface properties cannot specify a wider write-type, for simplicity. That could be considered as a future add-on with no BC breaks. 11. Provided an explanation of how to interpret the parent-hook access syntax. 12. Added an FAQ item explaining why a 'virtual' keyword is not feasible. Larger changes: 0. As noted a while ago, $field has been removed. 1. I've added an FAQ question regarding the parent::$foo::get() syntax, and why it is. 2. The $foo => expression shorthand has been removed. The legal shorthands are now: public string $foo { get => evaluates to a value; set => assigns this value; } 3. The set shorthand (with => ) now means "write this value instead". The non-shorthand version (set { } ) is always return void, so you have to assign the value yourself but you get more flexibility. Having updated the examples accordingly, I think this is actually a really nice and intuitive trade-off, as it makes the common transformation and validation cases (eg, in constructor promotion) even easier to follow with no redundancy. 4. On a set hook, the user may specify both a type and name, or neither. (That is, set {} or set (Foo $newFoo). If not specified, it defaults to the type of the property and $value, as before. 5. I restructured how the content in 2, 3, 4 is presented and moved some stuff around so that it flows more logically (I think). 6. The restrictions around references have been softened. Specifically, references are only disallowed if a backed property has a set hook. If it has only a get, or if it's a virtual property, references are allowed. We also added a future-scope section on a possible way to support assigning by reference in the future, if there is sufficient need. 7. Interfaces may now require a &get hook, or just 'get'. A class may use a &get hook on a 'get' interface declaration. This is the same logic as already exists for methods; we're just copying it. Hopefully the above changes should resolve most outstanding concerns. I do think the updated shorthand handling is better overall, so I'm happy with it. There also seems to be little consensus so far on naming this thing hooks vs accessors. Absent a consensus, we'll probably stick with hooks to avoid the effort of renaming all the things. Thank you everyone for the feedback so far, and if you still have some, please say so. (Even if it's just to say that you're happy with the RFC now so we feel more comfortable bringing it to a vote.) --Larry Garfield

Michał Brzuchalski

2 years ago
Hi Larry, pt., 8 mar 2024 o 16:55 Larry Garfield <larry@garfieldtech.com> napisał(a):
> Hi folks. Based on earlier discussions, we've made a number of changes to > the RFC that should address some of the concerns people raised. We also > had some very fruitful discussions off-list with several developers from > the Foundation, which led to what we feel are some solid improvements. > > https://wiki.php.net/rfc/property-hooks > >
This RFC looks awesome, thanks Larry and Ilija I love the functionality in its current shape. Thank you everyone for the feedback so far, and if you still have some,
> please say so. (Even if it's just to say that you're happy with the RFC > now so we feel more comfortable bringing it to a vote.) >
The only thing I don't like and can still be worked on is the reflection mechanism changes. The proposed methods isVirtual and getRawValue, setRawValue pair introduces a need to catch exceptions which could be eliminated by subtyping ReflectionProperty. Having these methods on a separate subtype allows returning a valid value. I realize this isn't trivial because for the last 2 days, I was thinking about giving it a name and TBH cannot figure out anything feasible. If this is not possible to put in understandable words then at least mention it in FAQ and why not. Cheers, Michał Marcin Brzuchalski

Larry Garfield

2 years ago
On Mon, Mar 11, 2024, at 8:35 AM, Michał Marcin Brzuchalski wrote:
> Hi Larry, > > pt., 8 mar 2024 o 16:55 Larry Garfield <larry@garfieldtech.com> napisał(a): >> Hi folks. Based on earlier discussions, we've made a number of changes to the RFC that should address some of the concerns people raised. We also had some very fruitful discussions off-list with several developers from the Foundation, which led to what we feel are some solid improvements. >> >> https://wiki.php.net/rfc/property-hooks >> > > This RFC looks awesome, thanks Larry and Ilija I love the functionality > in its current shape. > >> Thank you everyone for the feedback so far, and if you still have some, please say so. (Even if it's just to say that you're happy with the RFC now so we feel more comfortable bringing it to a vote.) > > The only thing I don't like and can still be worked on is the > reflection mechanism changes. > The proposed methods isVirtual and getRawValue, setRawValue pair > introduces a need to catch exceptions which could be eliminated by > subtyping ReflectionProperty. > Having these methods on a separate subtype allows returning a valid > value. > I realize this isn't trivial because for the last 2 days, I was > thinking about giving it a name and TBH cannot figure out anything > feasible. > If this is not possible to put in understandable words then at least > mention it in FAQ and why not. > > Cheers, > Michał Marcin Brzuchalski
Hm, interesting. I'll have to check with Ilija on feasibility. My question is, how would it eliminate it? Suppose we hypothetically have a "ReflectionPropertyWithHooks" reflection object. It has those three extra methods on it. But that means $rObject->getProperty() could return a ReflectionProperty or ReflectionPropertyWithHooks, and you don't know which it is. You'd have to do an instanceof check to know which type of property it is, and thus what methods are available. That doesn't seem any less clumsy (nor more, to be fair) than calling isVirtual(). $rProp = $rObject->getProperty('foo', $obj); $rProp->getValue(); // works always. if (!$rProp->isVirtual()) { $rProp->getRawValue(); // Works, may or may not be the same return as getValue() } vs. if (!$rProp instanceof VirtualProperty) { $rProp->getRawValue(); // Works. } That doesn't seem to be an improvement. If you omit the conditional, you still need a catch one way or the other. It just changes what gets thrown (an Exception vs an Error). What type of hierarchy were you thinking of that would help here? --Larry Garfield

Michał Brzuchalski

2 years ago
pon., 11 mar 2024 o 15:30 Larry Garfield <larry@garfieldtech.com> napisał(a):
> On Mon, Mar 11, 2024, at 8:35 AM, Michał Marcin Brzuchalski wrote: > > Hi Larry, > > > > pt., 8 mar 2024 o 16:55 Larry Garfield <larry@garfieldtech.com> > napisał(a): > >> Hi folks. Based on earlier discussions, we've made a number of changes > to the RFC that should address some of the concerns people raised. We also > had some very fruitful discussions off-list with several developers from > the Foundation, which led to what we feel are some solid improvements. > >> > >> https://wiki.php.net/rfc/property-hooks > >> > > > > This RFC looks awesome, thanks Larry and Ilija I love the functionality > > in its current shape. > > > >> Thank you everyone for the feedback so far, and if you still have some, > please say so. (Even if it's just to say that you're happy with the RFC > now so we feel more comfortable bringing it to a vote.) > > > > The only thing I don't like and can still be worked on is the > > reflection mechanism changes. > > The proposed methods isVirtual and getRawValue, setRawValue pair > > introduces a need to catch exceptions which could be eliminated by > > subtyping ReflectionProperty. > > Having these methods on a separate subtype allows returning a valid > > value. > > I realize this isn't trivial because for the last 2 days, I was > > thinking about giving it a name and TBH cannot figure out anything > > feasible. > > If this is not possible to put in understandable words then at least > > mention it in FAQ and why not. > > > > Cheers, > > Michał Marcin Brzuchalski > > Hm, interesting. I'll have to check with Ilija on feasibility. My > question is, how would it eliminate it? > > Suppose we hypothetically have a "ReflectionPropertyWithHooks" reflection > object. It has those three extra methods on it. But that means > $rObject->getProperty() could return a ReflectionProperty or > ReflectionPropertyWithHooks, and you don't know which it is. You'd have to > do an instanceof check to know which type of property it is, and thus what > methods are available. That doesn't seem any less clumsy (nor more, to be > fair) than calling isVirtual().
It is similar when you work with ReflectionType or ReflectionEnum, you always need to match against a certain type to ensure the code will behave predictably.
> $rProp = $rObject->getProperty('foo', $obj); > $rProp->getValue(); // works always. > > if (!$rProp->isVirtual()) { > $rProp->getRawValue(); // Works, may or may not be the same return as > getValue() > } > > vs. > > if (!$rProp instanceof VirtualProperty) { > $rProp->getRawValue(); // Works. > } > > That doesn't seem to be an improvement. If you omit the conditional, you > still need a catch one way or the other. It just changes what gets thrown > (an Exception vs an Error). What type of hierarchy were you thinking of > that would help here? >
My thinking was like: 1. if the property has hooks, only then calling getRawValue, setRawValue, or isVirtual make sense, 2. if the property has no hooks and is static calling getRawValue, or setRawValue always throws because of "On a static property, this method will always throw an error." 3. if the property is "virtual", calling getRawValue, or setRawValue always throws an error, 4. if the property is not "virtual", calling getValue, or setValue is safe and never throws, otherwise it may throw under some conditions related to certain hook presence. In conclusion, I thought that the presence of hooks introduces 3 new methods but some will always be thrown because of incorrect usage. Normally, I'd model it with subtypes to completely avoid try-catch blocks for the cost of a simple instanceof check which I consider much cleaner for the reader than a bunch of try-catch blocks. Remember about static analysis, each of them when checked will propose to handle possible exceptions. But as wrote before, I don't know how to model it well. Cheers, Michał Marcin Brzuchalski

Larry Garfield

2 years ago
On Mon, Mar 11, 2024, at 4:58 PM, Michał Marcin Brzuchalski wrote:
>> Hm, interesting. I'll have to check with Ilija on feasibility. My question is, how would it eliminate it? >> >> Suppose we hypothetically have a "ReflectionPropertyWithHooks" reflection object. It has those three extra methods on it. But that means $rObject->getProperty() could return a ReflectionProperty or ReflectionPropertyWithHooks, and you don't know which it is. You'd have to do an instanceof check to know which type of property it is, and thus what methods are available. That doesn't seem any less clumsy (nor more, to be fair) than calling isVirtual(). > > It is similar when you work with ReflectionType or ReflectionEnum, you > always need to match against a certain type to ensure the code will > behave predictably. > >> $rProp = $rObject->getProperty('foo', $obj); >> $rProp->getValue(); // works always. >> >> if (!$rProp->isVirtual()) { >> $rProp->getRawValue(); // Works, may or may not be the same return as getValue() >> } >> >> vs. >> >> if (!$rProp instanceof VirtualProperty) { >> $rProp->getRawValue(); // Works. >> } >> >> That doesn't seem to be an improvement. If you omit the conditional, you still need a catch one way or the other. It just changes what gets thrown (an Exception vs an Error). What type of hierarchy were you thinking of that would help here? > > My thinking was like: > 1. if the property has hooks, only then calling getRawValue, > setRawValue, or isVirtual make sense, > 2. if the property has no hooks and is static calling getRawValue, or > setRawValue always throws because of "On a static property, this method > will always throw an error." > 3. if the property is "virtual", calling getRawValue, or setRawValue > always throws an error, > 4. if the property is not "virtual", calling getValue, or setValue is > safe and never throws, otherwise it may throw under some conditions > related to certain hook presence. > > In conclusion, I thought that the presence of hooks introduces 3 new > methods but some will always be thrown because of incorrect usage. > Normally, I'd model it with subtypes to completely avoid try-catch > blocks for the cost of a simple instanceof check which I consider much > cleaner for the reader than a bunch of try-catch blocks. Remember about > static analysis, each of them when checked will propose to handle > possible exceptions. > > But as wrote before, I don't know how to model it well.
Agreed, though I don't know how to model it either. :-) If someone can figure out a way to do so before we go to a vote, we'll consider it. --Larry Garfield

Stephen Reay

2 years ago
> On 8 Mar 2024, at 22:53, Larry Garfield <larry@garfieldtech.com> wrote: > > Hi folks. Based on earlier discussions, we've made a number of changes to the RFC that should address some of the concerns people raised. We also had some very fruitful discussions off-list with several developers from the Foundation, which led to what we feel are some solid improvements. > > https://wiki.php.net/rfc/property-hooks > > Smaller changes: > > 1. We've added a PropertyHookType enum for use in reflection, which lets us get rid of a possible exception. > 2. get_mangled_object_vars()'s behavior is now defined to be the same as an array, ie, skip hooks. > 3. A final property with a final hook is no longer an error. It's just silently redundant, like for methods. > 5. Made explict what happens with recursive hook calls and method calls from a hook. The behavior here is the same as for __get/__set. > 6. Made support for #[\Override] explicit. > 7. Added an FAQ regarding the property-centric approach rather than method-centric approach. > 8. Clarified that the parent::$foo::get() syntax works on a parent property regardless of whether it has hooks. > 9. Clarified that untyped properties are supported. (Though, it's 2024, please don't use untyped properties.) > 10. Clarified that interface properties cannot specify a wider write-type, for simplicity. That could be considered as a future add-on with no BC breaks. > 11. Provided an explanation of how to interpret the parent-hook access syntax. > 12. Added an FAQ item explaining why a 'virtual' keyword is not feasible. > > Larger changes: > > 0. As noted a while ago, $field has been removed. > > 1. I've added an FAQ question regarding the parent::$foo::get() syntax, and why it is. > > 2. The $foo => expression shorthand has been removed. The legal shorthands are now: > > public string $foo { > get => evaluates to a value; > set => assigns this value; > } > > 3. The set shorthand (with => ) now means "write this value instead". The non-shorthand version (set { } ) is always return void, so you have to assign the value yourself but you get more flexibility. Having updated the examples accordingly, I think this is actually a really nice and intuitive trade-off, as it makes the common transformation and validation cases (eg, in constructor promotion) even easier to follow with no redundancy. > > 4. On a set hook, the user may specify both a type and name, or neither. (That is, set {} or set (Foo $newFoo). If not specified, it defaults to the type of the property and $value, as before. > > 5. I restructured how the content in 2, 3, 4 is presented and moved some stuff around so that it flows more logically (I think). > > 6. The restrictions around references have been softened. Specifically, references are only disallowed if a backed property has a set hook. If it has only a get, or if it's a virtual property, references are allowed. We also added a future-scope section on a possible way to support assigning by reference in the future, if there is sufficient need. > > 7. Interfaces may now require a &get hook, or just 'get'. A class may use a &get hook on a 'get' interface declaration. This is the same logic as already exists for methods; we're just copying it. > > Hopefully the above changes should resolve most outstanding concerns. I do think the updated shorthand handling is better overall, so I'm happy with it. > > There also seems to be little consensus so far on naming this thing hooks vs accessors. Absent a consensus, we'll probably stick with hooks to avoid the effort of renaming all the things. > > Thank you everyone for the feedback so far, and if you still have some, please say so. (Even if it's just to say that you're happy with the RFC now so we feel more comfortable bringing it to a vote.) > > --Larry Garfield >
Hi Larry Thanks again for both of your work on this, I'm really hopeful this passes. Was there ever any further discussion/resolution/decision about the use an explicit `virtual` keyword, and the related flag for creation of a backing store? I thought it was discussed by several people but I don't recall seeing any eventual consensus, and it looks to my eye that it hasn't changed from the original proposal: i.e. it's 'magic' and `$this->{__PROPERTY__}` won't work? Is that correct? Cheers Stephen

Larry Garfield

2 years ago
On Tue, Mar 12, 2024, at 8:49 AM, Stephen Reay wrote:
> Hi Larry > > Thanks again for both of your work on this, I'm really hopeful this passes. > > Was there ever any further discussion/resolution/decision about the use > an explicit `virtual` keyword, and the related flag for creation of a > backing store? I thought it was discussed by several people but I don't > recall seeing any eventual consensus, and it looks to my eye that it > hasn't changed from the original proposal: i.e. it's 'magic' and > `$this->{__PROPERTY__}` won't work? > > Is that correct?
We looked into `virtual`, and considered it, but determined that it wouldn't actually work because of inheritance. Details are in a new FAQ entry: https://wiki.php.net/rfc/property-hooks#why_no_explicit_virtual_flag --Larry Garfield

Alexandru Pătrănescu

2 years ago
On Tue, Mar 12, 2024 at 4:36 PM Larry Garfield <larry@garfieldtech.com> wrote:
> On Tue, Mar 12, 2024, at 8:49 AM, Stephen Reay wrote: > > > Hi Larry > > > > Thanks again for both of your work on this, I'm really hopeful this > passes. > > > > Was there ever any further discussion/resolution/decision about the use > > an explicit `virtual` keyword, and the related flag for creation of a > > backing store? I thought it was discussed by several people but I don't > > recall seeing any eventual consensus, and it looks to my eye that it > > hasn't changed from the original proposal: i.e. it's 'magic' and > > `$this->{__PROPERTY__}` won't work? > > > > Is that correct? > > We looked into `virtual`, and considered it, but determined that it > wouldn't actually work because of inheritance. Details are in a new FAQ > entry: > > https://wiki.php.net/rfc/property-hooks#why_no_explicit_virtual_flag > > >
Nice that you added the details there why explicit virtual is not a good idea. So, if the parent class changes between virtual and non-virtual, while the child class is virtual, the only tiny BC break will be in the reflection as we will see a change between virtual and non-virtual? Alex

Larry Garfield

2 years ago
On Tue, Mar 12, 2024, at 4:53 PM, Alexandru Pătrănescu wrote:
> On Tue, Mar 12, 2024 at 4:36 PM Larry Garfield <larry@garfieldtech.com> wrote: >> On Tue, Mar 12, 2024, at 8:49 AM, Stephen Reay wrote: >> >> > Hi Larry >> > >> > Thanks again for both of your work on this, I'm really hopeful this passes. >> > >> > Was there ever any further discussion/resolution/decision about the use >> > an explicit `virtual` keyword, and the related flag for creation of a >> > backing store? I thought it was discussed by several people but I don't >> > recall seeing any eventual consensus, and it looks to my eye that it >> > hasn't changed from the original proposal: i.e. it's 'magic' and >> > `$this->{__PROPERTY__}` won't work? >> > >> > Is that correct? >> >> We looked into `virtual`, and considered it, but determined that it wouldn't actually work because of inheritance. Details are in a new FAQ entry: >> >> https://wiki.php.net/rfc/property-hooks#why_no_explicit_virtual_flag >> >> > > Nice that you added the details there why explicit virtual is not a good idea. > > So, if the parent class changes between virtual and non-virtual, while > the child class is virtual, the only tiny BC break will be in the > reflection as we will see a change between virtual and non-virtual? > > Alex
I believe that is correct, yes. --Larry Garfield

Rowan Tommins [IMSoP]

2 years ago
On 08/03/2024 15:53, Larry Garfield wrote:
> Hi folks. Based on earlier discussions, we've made a number of changes > to the RFC that should address some of the concerns people raised. We > also had some very fruitful discussions off-list with several developers > from the Foundation, which led to what we feel are some solid > improvements. > > https://wiki.php.net/rfc/property-hooks
Hi Larry, Thanks again for the continuing hard work on this!
> 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.
> 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.
> 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.
> 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;    } }
> 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(); Once again, I would like to make the case that asymmetric types are an unnecessary complication that should be left to Future Scope. The fact that none of the other languages referenced have such a feature should also give us pause. There's nothing to stop us being the first to innovate a feature, but we should be extra cautious when doing so, with no previous experience to learn from. It also means there is no expectation from users coming from other languages that this will be possible. If it genuinely seems useful, it can be added in a follow-up RFC, or even a later version of PHP, with little impact on the rest of the feature. But if we add it now and regret it, or some detail of its implementation, we will be stuck with it forever. Regards,
-- Rowan Tommins [IMSoP]

Larry Garfield

2 years ago
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

Rowan Tommins [IMSoP]

2 years ago
On 12/03/2024 22:43, Larry Garfield wrote:
> 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.
I guess I'm looking at it more from the user's point of view: it's very rare with __get and __set to have a method that sometimes accesses the "real" property, and sometimes goes through the "hook". Either there is no real property, or the property has private/protected scope, so any method on the classes sees the "real" property *regardless* of access via the hook. I think it would be more helpful to justify this design on its own merits, particularly because it's a significant difference from other languages (which either don't have a "real property" behind the hooks, or in Kotlin's case allow access to it only *directly* inside the hook definitions, via the "field" keyword).
> 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.
I get the impression that to you, it's a given that a "virtual property" is something clearly distinct from a "property with hooks", and that users will consciously decide between one and the other. This isn't my expectation; based on what people are used to from existing features, and other languages, I expect users to see this as an obvious starting point for defining a hooked property: private int $_foo; public int $foo { get => $this->_foo; set { $this->_foo = $value; } { And this as a convenient short-hand for exactly the same thing: public int $foo { get => $this->foo; set { $this->foo = $value; } } Choosing one or the other won't feel like "jumping through a hoop", and the ability to use an &get hook with one and not the other will simply seem like a weird oddity.
> 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.
I don't think this is true. Both of these are, in the terms of the RFC, "virtual properties": public Something $proxied { get => $this->otherObject->thing; set { $this->otherObject->thing = $value; } }; public Money $price; public int $pricePence { get => $this->price->asPence(); set { $this->price = Money::fromPence($value); } } I can also imagine generated classes with "virtual" properties which call out to generic "getCached" and "setAndClearCache" methods doing the job of this pair of __get and __set methods: https://github.com/yiisoft/yii2/blob/master/framework/db/BaseActiveRecord.php#L274
> 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
Awesome! The RFC should probably highlight this, as it gives a significant extra option for array properties. (Also, good to know 3v4l has a copy of the branch; I hadn't thought to check.) Regards,
-- Rowan Tommins [IMSoP]

Larry Garfield

2 years ago
On Wed, Mar 13, 2024, at 10:26 PM, Rowan Tommins [IMSoP] wrote:
> On 12/03/2024 22:43, Larry Garfield wrote: >> 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. > > > I guess I'm looking at it more from the user's point of view: it's very > rare with __get and __set to have a method that sometimes accesses the > "real" property, and sometimes goes through the "hook". Either there is > no real property, or the property has private/protected scope, so any > method on the classes sees the "real" property *regardless* of access > via the hook. > > I think it would be more helpful to justify this design on its own > merits, particularly because it's a significant difference from other > languages (which either don't have a "real property" behind the hooks, > or in Kotlin's case allow access to it only *directly* inside the hook > definitions, via the "field" keyword).
I'm not sure I follow. The behavior we have currently is very close to how Kotlin works, from a user perspective. (The internal implementation is backwards from Kotlin, but that doesn't matter to the user.) I've lost track of which specific issue you have an issue with or would want changed. The guards to prevent an infinite loop are necessary, for the same reasons as they are necessary for __get/__set. We couldn't use a backing field otherwise, without some other syntax. (This is where Kotlin uses 'field'.) So, I'm not really sure what we're discussing at this point. What specific changes are you suggesting?
>> 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 > > > Awesome! The RFC should probably highlight this, as it gives a > significant extra option for array properties.
Updated. I may try to rewrite the array and references section this weekend, as with the changes in the design to be more permissive I'm not sure it's entirely clear anymore what the net result is. --Larry Garfield

Rowan Tommins [IMSoP]

2 years ago
On 15 March 2024 17:11:29 GMT, Larry Garfield <larry@garfieldtech.com> wrote:
>On Wed, Mar 13, 2024, at 10:26 PM, Rowan Tommins [IMSoP] wrote: >> I think it would be more helpful to justify this design on its own >> merits, particularly because it's a significant difference from other >> languages (which either don't have a "real property" behind the hooks, >> or in Kotlin's case allow access to it only *directly* inside the hook >> definitions, via the "field" keyword). > >I'm not sure I follow. The behavior we have currently is very close to how Kotlin works, from a user perspective.
Unless I'm misunderstanding something, the backing field in Kotlin is accessible only inside the hooks, nowhere else. I don't know what would happen if a hook caused a recursive call to itself, but there's no mention in the docs of it bypassing the hooks, only this:
> This backing field can be referenced in the accessors using the `field` identifier
and
> The `field` identifier can only be used in the accessors of the property.
And then a section explaining that more complex hooks should use a separate backing property - which is the only option in C#, and roughly what people would do in PHP today with __get and __set. Kotlin does have a special syntax for "delegating" hooks, but looking at the examples, they do not use the backing field at all, they have to provide their own storage.
>I've lost track of which specific issue you have an issue with or would want changed. The guards to prevent an infinite loop are necessary, for the same reasons as they are necessary for __get/__set.
I understand that *something* needs to happen if a recursive call happens, but it could just be an error, like any other unbounded recursion. I can also understand the temptation to make it something more useful than an error, and provide a way to access the "backing field" / "raw value" from outside the hook. But it does lead to something quite surprising: the same line of code does different things depending on how it is called. I doubt many people have ever discovered that __get and __set work that way, since as far as I can see it's only possible to use deliberately if you're dynamically adding and unsetting properties inside your class. So, I don't necessarily think hooks working that way is the wrong decision, I just think it's a decision we should make consciously, not one that's obvious. Regards, Rowan Tommins [IMSoP]

Larry Garfield

2 years ago
On Fri, Mar 15, 2024, at 11:25 PM, Rowan Tommins [IMSoP] wrote:
> On 15 March 2024 17:11:29 GMT, Larry Garfield <larry@garfieldtech.com> wrote: >>On Wed, Mar 13, 2024, at 10:26 PM, Rowan Tommins [IMSoP] wrote: >>> I think it would be more helpful to justify this design on its own >>> merits, particularly because it's a significant difference from other >>> languages (which either don't have a "real property" behind the hooks, >>> or in Kotlin's case allow access to it only *directly* inside the hook >>> definitions, via the "field" keyword). >> >>I'm not sure I follow. The behavior we have currently is very close to how Kotlin works, from a user perspective. > > > Unless I'm misunderstanding something, the backing field in Kotlin is > accessible only inside the hooks, nowhere else. I don't know what would > happen if a hook caused a recursive call to itself, but there's no > mention in the docs of it bypassing the hooks, only this: > >> This backing field can be referenced in the accessors using the `field` identifier > > and > >> The `field` identifier can only be used in the accessors of the property. > > And then a section explaining that more complex hooks should use a > separate backing property - which is the only option in C#, and roughly > what people would do in PHP today with __get and __set. > > Kotlin does have a special syntax for "delegating" hooks, but looking > at the examples, they do not use the backing field at all, they have to > provide their own storage. > > > >>I've lost track of which specific issue you have an issue with or would want changed. The guards to prevent an infinite loop are necessary, for the same reasons as they are necessary for __get/__set. > > I understand that *something* needs to happen if a recursive call > happens, but it could just be an error, like any other unbounded > recursion. > > I can also understand the temptation to make it something more useful > than an error, and provide a way to access the "backing field" / "raw > value" from outside the hook. But it does lead to something quite > surprising: the same line of code does different things depending on > how it is called. > > I doubt many people have ever discovered that __get and __set work that > way, since as far as I can see it's only possible to use deliberately > if you're dynamically adding and unsetting properties inside your class. > > So, I don't necessarily think hooks working that way is the wrong > decision, I just think it's a decision we should make consciously, not > one that's obvious.
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; } } So disabling the hooks from within the hooks seems like the only logical solution there. (Short of bringing back $field and making it mandatory, which is actually much harder than it sounds because of the ++ et al operators that would need to be supported.) The other case then becomes: class Foo { public string $a { get => $this->expensive ??= $this->compute(); set { if (strlen($value) < 50) throw new Exception(); $this->expensive = $value; } } public function compute() { $start = $this->expensive ?? 'a'; return $start . 'b'; } } Inside compute(), the logic requires reading from $this->expensive. If we have no guard, that would cause an infinite loop. If the guard extends down the call stack, then the loop is eliminated. That does mean, as you note, that if you call $foo->compute(), the first call to $this->expensive will invoke the get hook, but subsequent calls to it will not. That may seem odd, but in practice, we do not see any other alternative that doesn't make infinite loops very easy to write. This is, of course, a highly contrived example. In practice, I don't expect it to come up much in the first place. It was this logic that led us to the current implementation: Once you're within the callstack of a hook, you bypass that property's hooks. We then noted that __get/__set have essentially the same guard logic. It doesn't come up often because, again as you note, it's only relevant in even more contrived examples. So yes, the current logic is very deliberate and based on a process of elimination to arrive at what we feel is the only logical design. Likely the same process of elimination that led to the behavior of the guards on __get/__set, which is why they are essentially the same. 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.) In theory we could also forbid accessing a property within the call stack of its hooks; in that case, the above code would simply error. However, that is a less-optimal solution because then compute() is only sometimes callable, depending on the callstack. That is no less weird than $this->expensive skipping hooks only sometimes. It would also be inconsistent with how __get/__set work, which makes hooks less capable and less consistent. That's why we don't think that's a good way of doing it. Also, we've rewritten the references and arrays sections in the RFC. Nothing actually changed in the implementation, but it should be a lot clearer now that only a fairly small subset of impossible situations are blocked. Most things "just work," and when they don't, they (again) don't for a logical reason that parallels the behavior of the language elsewhere. There's even some nice summary tables. :-) --Larry Garfield

Rowan Tommins [IMSoP]

2 years ago
On 16 March 2024 00:19:57 GMT, Larry Garfield <larry@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.
> 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. Regards, Rowan Tommins [IMSoP]

Ilija Tovilo

2 years ago
Hi Rowan On Sat, Mar 16, 2024 at 9:32 AM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
> > On 16 March 2024 00:19:57 GMT, Larry Garfield <larry@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

Derick Rethans

2 years ago
On Fri, 8 Mar 2024, Larry Garfield wrote:
> Hi folks. Based on earlier discussions, we've made a number of > changes to the RFC that should address some of the concerns people > raised. We also had some very fruitful discussions off-list with > several developers from the Foundation, which led to what we feel are > some solid improvements. > > https://wiki.php.net/rfc/property-hooks
Some comments and questions: Be aware, the detection logic works on $this->[propertyName] directly at compile time, not on dynamic forms of it like $prop = 'beep'; $this->$prop. That will not trigger a backing value. How can that not cause issues? The set hook's return type is unspecified, and will silently be treated as void. What happens if you *do* specify a return type? Will it Error? Implicit ''set'' parameter If the write-type of a property is the same as its defined type (this is the common case), then the argument may be omitted entirely. … If the parameter is not specified, it defaults to $value. I am not a fan of this "magical" behaviour. Do we *need* this short cut, and the following "Short-set"? With asymmetric visibility that was previously proposed, the example can be further simplified. But it isn't here, so why is this example (and the next one) in the RFC? :-) Interaction with constructor property promotion … In particular, the shorthand version of hook bodies and the ability to call out to private methods if they get complicated partially obviate the concern about syntactic complexity. Although that is true, it *does* add more complexity in tools that needs to parse PHP, as there is now another piece of new syntax that needs to be added (and tested with). ReflectionProperty has several new methods to work with hooks. getHooks(): array returns an array of \ReflectionMethod objects keyed by the hook they are for. What will the name for the &get hook be? And shouldn't there be an enum case for that as well? cheers, Derick

Larry Garfield

2 years ago
On Tue, Mar 19, 2024, at 11:16 AM, Derick Rethans wrote:
> On Fri, 8 Mar 2024, Larry Garfield wrote: > >> Hi folks. Based on earlier discussions, we've made a number of >> changes to the RFC that should address some of the concerns people >> raised. We also had some very fruitful discussions off-list with >> several developers from the Foundation, which led to what we feel are >> some solid improvements. >> >> https://wiki.php.net/rfc/property-hooks > > Some comments and questions: > > Be aware, the detection logic works on $this->[propertyName] directly at > compile time, not on dynamic forms of it like $prop = 'beep'; > $this->$prop. That will not trigger a backing value. > > How can that not cause issues?
For most uses that should be fine. But it's more that trying to do anything else would be vastly more complicated and error prone for the implementation. And most people hated the alternate $field variable from Kotlin.
> The set hook's return type is unspecified, and will silently be > treated as void. > > What happens if you *do* specify a return type? Will it Error?
Yes, it's a parse error.
> Implicit ''set'' parameter > > If the write-type of a property is the same as its defined type > (this is the common case), then the argument may be omitted > entirely. > > … > > If the parameter is not specified, it defaults to $value. > > I am not a fan of this "magical" behaviour. Do we *need* this short cut, > and the following "Short-set"?
We believe we do. One of the goals of the RFC is to reduce and avoid verbose boilerplate. The short-hands should be the common case, in practice. The only reason one would have to specify a set parameter is if there was a good reason to change the variable name or widen the set type. Both of those are a-typical situations.
> With asymmetric visibility that was previously proposed, the > example can be further simplified. > > But it isn't here, so why is this example (and the next one) in the RFC? > :-)
Many/most languages with accessors also have asymmetric visibility. Nikita's original RFC also combined them all into one. The two RFCs are not mutually dependent, but are mutually-supportive, by design. So this comes under the "why didn't we include feature X" heading, with an answer "that's in a separate RFC."
> Interaction with constructor property promotion > > … In particular, the shorthand version of hook bodies and the > ability to call out to private methods if they get complicated > partially obviate the concern about syntactic complexity. > > Although that is true, it *does* add more complexity in tools that needs > to parse PHP, as there is now another piece of new syntax that needs to > be added (and tested with).
That's true, but it's also inherently true of hooks themselves. And any other syntax improvement to the language.
> ReflectionProperty has several new methods to work with hooks. > > getHooks(): array returns an array of \ReflectionMethod objects > keyed by the hook they are for. > > What will the name for the &get hook be? And shouldn't there be an enum > case for that as well?
&get isn't its own hook. It's still a "get" hook, it just returns by reference. So it will be available with the "Get" enum value. If the return-by-ref status needs to be known for whatever reason, that is already available on the ReflectionMethod object that is returned. --Larry Garfield

Robert Landers

2 years ago
Hello, I'm a bit confused on inheritance. In the following example of a proxy, do I need to be aware of a parent's hook and handle it specially? class Loud { public string $name { get { return strtoupper($this->name); } } } class LoudProxy extends Loud { public string $name { get { // detected the parent has a hook? // $return = parent::$name::get(); // do something with return // return $return; } } } what happens if the Loud class later removes its hook implementation (ex: moves it to the set hook)? Will my proxy now cause an error? Would simply calling $this->name call the parents hook? Robert Landers Software Engineer Utrecht NL

Larry Garfield

2 years ago
On Thu, Mar 21, 2024, at 9:10 AM, Robert Landers wrote:
> Hello, > > I'm a bit confused on inheritance. In the following example of a > proxy, do I need to be aware of a parent's hook and handle it > specially? > > class Loud > { > public string $name { > get { > return strtoupper($this->name); > } > } > } > > class LoudProxy extends Loud > { > public string $name { > get { > // detected the parent has a hook? // > $return = parent::$name::get(); > // do something with return // > return $return; > } > } > } > > what happens if the Loud class later removes its hook implementation > (ex: moves it to the set hook)? Will my proxy now cause an error? > > Would simply calling $this->name call the parents hook?
Per the RFC: "If there is no hook on the parent property, its default get/set behavior will be used. " so parent::$name::get() will "read the parent property", which will go through a hook if one is defined, and just read the raw value if not. So there is no detection logic needed, and the parent can add/remove a hook without affecting the child. Calling $this->name in LoudProxy's get hook will access backing property on LoudProxy itself, ignoring the parent entirely. --Larry Garfield

Robert Landers

2 years ago
On Thu, Mar 21, 2024 at 5:15 PM Larry Garfield <larry@garfieldtech.com> wrote:
> > On Thu, Mar 21, 2024, at 9:10 AM, Robert Landers wrote: > > Hello, > > > > I'm a bit confused on inheritance. In the following example of a > > proxy, do I need to be aware of a parent's hook and handle it > > specially? > > > > class Loud > > { > > public string $name { > > get { > > return strtoupper($this->name); > > } > > } > > } > > > > class LoudProxy extends Loud > > { > > public string $name { > > get { > > // detected the parent has a hook? // > > $return = parent::$name::get(); > > // do something with return // > > return $return; > > } > > } > > } > > > > what happens if the Loud class later removes its hook implementation > > (ex: moves it to the set hook)? Will my proxy now cause an error? > > > > Would simply calling $this->name call the parents hook? > > Per the RFC: > > "If there is no hook on the parent property, its default get/set behavior will be used. " > > so parent::$name::get() will "read the parent property", which will go through a hook if one is defined, and just read the raw value if not. So there is no detection logic needed, and the parent can add/remove a hook without affecting the child. > > Calling $this->name in LoudProxy's get hook will access backing property on LoudProxy itself, ignoring the parent entirely. > > --Larry Garfield
Awesome! Thanks! I just wasn't sure what "default" meant in this context. Robert Landers Software Engineer Utrecht NL

Lynn

2 years ago
On Wed, Feb 21, 2024 at 7:58 PM Larry Garfield <larry@garfieldtech.com> wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks > > It’s long, but that’s because we’re handling every edge case we could > think of. Properties involve dealing with both references and inheritance, > both of which have complex implications. We believe we’ve identified the > most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: > Originally, this RFC was called “property accessors,” which is the > terminology used by most languages. During early development, when we had > 4 accessors like Swift, we changed the name to “hooks” to better indicate > that one was “hooking into” the property lifecycle. However, later > refinement brought it back down to 2 operations, get and set. That makes > the “hooks” name less applicable, and inconsistent with what other > languages call it. > > However, changing it back at this point would be a non-small amount of > grunt work. There would be no functional changes from doing so, but it’s > lots of renaming things both in the PR and the RFC. We are willing to do so > if the consensus is that it would be beneficial, but want to ask before > putting in the effort. > > -- > Larry Garfield > larry@garfieldtech.com
In regards to arrays, what about additional operations next to get/set? I doubt this solution will cover all the use-cases or perhaps even over-complicate things, just throwing the idea out there. ```php class Test { private array $_myData = []; public array $myData { get => $this->_myData; append => $this->_myData[] = $value; } } ``` Thinking about the other post about offset and containers ( https://github.com/Girgias/php-rfcs/blob/master/container -offset-behaviour.md): ```php class Test { private array $_myData = []; public array $myData { get => $this->_myData; append => $this->_myData[] = $value; write_dimension => $this->_myData[$offset] = $value; } } ``` Is this issue restricted to arrays only? From my understanding objects functioning as arrays are already by reference and thus should not suffer from this? ```php class Test { private ArrayObject $_myData; public ArrayObject $myData { get => $this->_myData; } public function __construct() { $this->_myData = new ArrayObject(); } } // would this work without issues? $obj = new Test(); $obj->myData[] = 'test'; ```

Larry Garfield

2 years ago
On Mon, Mar 18, 2024, at 9:13 AM, Lynn wrote:
> In regards to arrays, what about additional operations next to get/set? > I doubt this solution will cover all the use-cases or perhaps even > over-complicate things, just throwing the idea out there. > > ```php > class Test { > private array $_myData = []; > public array $myData { > get => $this->_myData; > append => $this->_myData[] = $value; > } > } > ``` > > Thinking about the other post about offset and containers > (https://github.com/Girgias/php-rfcs/blob/master/container-offset-behaviour.md): > ```php > class Test { > private array $_myData = []; > public array $myData { > get => $this->_myData; > append => $this->_myData[] = $value; > write_dimension => $this->_myData[$offset] = $value; > } > } > ```
Those hooks may be possible; we'd have to try it and see. However, they also wouldn't be able to fully emulate arrays. $foo->bar['baz'][] = 'beep' could get very weird. Those wouldn't cover unsetting. Array functions like array_splice() still wouldn't work. Basically, there will never be a way to make arrays 100% transparent with hooks. That's why the RFC recommends still using a method for array modification, as that's already a very well-understood and flexible approach. Fortunately, as currently written (append and write_dimension are forbidden), those additional hooks could be considered and added in their own RFC in the future with no BC break. Whether or not they make sense or cover "enough" use cases is a question that could be answered in the future, after Gina's RFC passes. So on this one, I think "punt" is the best option for now. It can be safely revisited in the future.
> Is this issue restricted to arrays only? From my understanding objects > functioning as arrays are already by reference and thus should not > suffer from this? > ```php > class Test { > private ArrayObject $_myData; > public ArrayObject $myData { > get => $this->_myData; > } > > public function __construct() { > $this->_myData = new ArrayObject(); > } > } > > // would this work without issues? > $obj = new Test(); > $obj->myData[] = 'test'; > ```
Mostly correct. Objects pass by handle, not by reference. (You can pass an object by reference instead, and it behaves subtly differently.) But the net effect is the same. Your sample code there would run fine. --Larry Garfield

Jakob Givoni

2 years ago
On Wed, Feb 21, 2024, 19:57 Larry Garfield <larry@garfieldtech.com> wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. >
> It’s long, but that’s because we’re handling every edge case we could > think of. Properties involve dealing with both references and inheritance, > both of which have complex implications. We believe we’ve identified the > most logical handling for all cases, though. > > Note the FAQ question at the end, which explains some design choices. > > There’s one outstanding question, which is slightly painful to ask: > Originally, this RFC was called “property accessors,” which is the > terminology used by most languages. During early development, when we had > 4 accessors like Swift, we changed the name to “hooks” to better indicate > that one was “hooking into” the property lifecycle. However, later > refinement brought it back down to 2 operations, get and set. That makes > the “hooks” name less applicable, and inconsistent with what other > languages call it. > > However, changing it back at this point would be a non-small amount of > grunt work. There would be no functional changes from doing so, but it’s > lots of renaming things both in the PR and the RFC. We are willing to do so > if the consensus is that it would be beneficial, but want to ask before > putting in the effort. > > -- > Larry Garfield > larry@garfieldtech.com
Hi, thanks for the RFC and the effort put into trying to make it palatable to skeptical minds! After reading most of the discussion in this thread I believe that the RFC in its current form can work and that I will get used to it's "peculiarities", but an idea occurred to me that may have some advantages, so here goes: Use the "set" keyword that you've already introduced to set the raw value of a "backed" property: public int $name { set { set strtoupper($value); } } Or when used in short form: public int $name { set => set strtoupper($value); } Advantages in no particular order: 1. Shorter than $this->name 2. No magic $field 3. Short and long form works the same Disadvantage: "Set" can only be used to set the raw value inside the hook method itself. Or maybe that's a good thing too. To be honest, I don't love that $this->name sometimes goes through the hook and sometimes not. I'd prefer if the raw value could only be accessed inside the hooks or via a special syntax like f.ex. $this->name:raw If there are any use cases or technical details that I've missed that would make this syntax unfavourable, I apologize. Another observation (I apologize for being late to the game but it was a long RFC and thread to read through): What would happen if we stopped talking about virtual vs. backed properties? Couldn't we just treat a property that was never set the same as any other uninitialized property? What I mean is, that if you try to access the raw value of a property with a set hook that never sets its own raw value, you'd get either null or Typed property [...] must not be accessed before initialization, just like you'd expect if you're already used to modern php. Of course you'd just write your code correctly so that that never happens. It's already the case that uninitialized properties are omitted when serializing the object so there would be no difference there either. The advantage here would be that there's no need to detect the virtual or backed nature of the property at compile time and the RFC would be a lot shorter. Thank you for your consideration! Best, Jakob

Larry Garfield

2 years ago
On Tue, Mar 26, 2024, at 8:18 PM, Jakob Givoni wrote:
> Hi, thanks for the RFC and the effort put into trying to make it > palatable to skeptical minds! > > After reading most of the discussion in this thread I believe that the > RFC in its current form can work and that I will get used to it's > "peculiarities", but an idea occurred to me that may have some > advantages, so here goes: > > Use the "set" keyword that you've already introduced to set the raw > value of a "backed" property: > > public int $name { > set { > set strtoupper($value); > } > } > Or when used in short form: > > public int $name { > set => set strtoupper($value); > } > > Advantages in no particular order: > 1. Shorter than $this->name > 2. No magic $field > 3. Short and long form works the same > > Disadvantage: "Set" can only be used to set the raw value inside the > hook method itself. Or maybe that's a good thing too. To be honest, I > don't love that $this->name sometimes goes through the hook and > sometimes not. I'd prefer if the raw value could only be accessed > inside the hooks or via a special syntax like f.ex. $this->name:raw > > If there are any use cases or technical details that I've missed that > would make this syntax unfavourable, I apologize.
Interesting idea. Not being able to write the raw value except in the set hook isn't a bug, but an important feature, so that's not a downside. (Modulo reflection, which is a reasonable back-door.) However, there's a few other disadvantages that probably make it not worth it. 1. `set` is not actually a keyword at the moment. It's contextually parsed in the lexer, so it doesn't preclude using `set` as a constant or function name the way a full keyword does. (PHP has many of these context-only keywords.) Making it a keyword inside the body of the hook would do that, however. 2. Like $field, it would be a syntax you just "have to know". Most people seem to hate that idea, right or wrong. 3. Like the considered syntaxes for parent-access, it wouldn't be possible to do anything but a direct write. So `set => set++` wouldn't be possible, whereas with $this->prop all existing operations should "just work." 4. Would we then also want a `get` keyword in the get hook to be parallel? What does that even do there? It would have the same implications as point 3 in get, so we're back to $field by a different spelling. So it's an interesting concept, but the knock-on effects would lead to a lot more complications.
> Another observation (I apologize for being late to the game but it was > a long RFC and thread to read through): > > What would happen if we stopped talking about virtual vs. backed > properties? Couldn't we just treat a property that was never set the > same as any other uninitialized property? > What I mean is, that if you try to access the raw value of a property > with a set hook that never sets its own raw value, you'd get either > null or Typed property [...] must not be accessed before > initialization, just like you'd expect if you're already used to modern > php. Of course you'd just write your code correctly so that that never > happens. It's already the case that uninitialized properties are > omitted when serializing the object so there would be no difference > there either. > > The advantage here would be that there's no need to detect the virtual > or backed nature of the property at compile time and the RFC would be a > lot shorter.
Unfortunately the backed-vs-virtual distinction is quite important at an implementation level for a few reasons. 1. A backed property reserves memory space for that property. A virtual property does not. Making virtual properties "unused backed" properties would increase memory usage for values that would never be usable. 2. There would be no realistic way to differentiate between a get-only virtual property with no storage, and a backed property that just happens to have a get hook but no set hook. Meaning you would be able to write to an otherwise-inaccessible backing value of the property. 3. That would then appear in serialization, even though it's impossible to get to from code without using reflection. Which is just all kinds of confusing. So for practical reasons, the distinction isn't just a user-facing difference but an important engine-level distinction we cannot avoid. Cheers. --Larry Garfield

Larry Garfield

2 years ago
On Wed, Feb 21, 2024, at 12:55 PM, Larry Garfield wrote:
> Hello again, fine Internalians. > > After much on-again/off-again work, Ilija and I are back with a more > polished property access hooks/interface properties RFC. It’s 99% > unchanged from last summer; the PR is now essentially complete and more > robust, and we were able to squish the last remaining edge cases. > > Baring any major changes, we plan to bring this to a vote in mid-March. > > https://wiki.php.net/rfc/property-hooks
Courtesy heads up: Baring any major changes, we are planning to call the vote in a little over a week, on the 8th/9th of April. (Ilija still has some tests he wants to finalize, and I will be out of town next weekend.) --Larry Garfield