[RFC] Readonly property hooks

php.internals

Larry Garfield

1 year ago
As Nick has graciously provided an implementation, we would like to open discussion on this very small RFC to allow `readonly` on backed properties even if they have a hook defined. https://wiki.php.net/rfc/readonly_hooks
-- Larry Garfield larry@garfieldtech.com

Volker Dusch

1 year ago
Hey Larry, Couple points from a first read and from trying to run the examples. a) From the "ProductFromDB" i get:
> Fatal error: Uncaught TypeError: LazyProduct::$category::get(): Return
value must be of type Category, none returned in ... I assume you're missing a return statement here? Or is there something I'm missing? b) Minor wording gripe in the proposal section
> On the other hand, there is no shortage of dumb things that people can do
with PHP already. While I don't disagree that PHP gives people a lot of freedom in how they want to write their code, I find it a bit crude for an RFC to phrase it like that. And the __get comparison is strong enough to stand on its own. c)
> That is, we feel, an entirely reasonable use of hooks, and would allow
for lazy-load behavior per-property on readonly classes. I might be misunderstanding the sentence here, but on-demand/Lazy initialization of properties on readonly classes is already possible in classic getter/setter classes. Do you mean you want to have parity to this behavior when using hooks? I'm all for it, I just feel the sentence says this enables something that wasn't possible before but if you mean this in scope property access it makes sense to me. d) PositivePoint Example doesn't compile against 8.4, master, or against NickSdot:readonly-hooks Can you make this a script that runs and shows expected output so that readers don't have to assume this is supposed to do or run it? e) Backward Incompatible Changes Section is not filled in yet f) Date: 2024-07-10 Is this correct? I know you created the page back then, but was there a discussion already that I wasn't able to find? Kind Regards, Volker On Sun, Jun 8, 2025 at 6:18 AM Larry Garfield <larry@garfieldtech.com> wrote:
> As Nick has graciously provided an implementation, we would like to open > discussion on this very small RFC to allow `readonly` on backed properties > even if they have a hook defined. > > https://wiki.php.net/rfc/readonly_hooks > > -- > Larry Garfield > larry@garfieldtech.com >
-- Volker Dusch Head of Engineering Tideways GmbH Königswinterer Str. 116 53227 Bonn https://tideways.io/imprint Sitz der Gesellschaft: Bonn Geschäftsführer: Benjamin Außenhofer (geb. Eberlei) Registergericht: Amtsgericht Bonn, HRB 22127

Nick

1 year ago
Hey Volker,
> On 8. Jun 2025, at 19:17, Volker Dusch <volker@tideways-gmbh.com> wrote: > > a) From the "ProductFromDB" i get: > > > Fatal error: Uncaught TypeError: LazyProduct::$category::get(): Return value must be of type Category, none returned in ... > > I assume you're missing a return statement here? Or is there something I'm missing? > > d) PositivePoint > > Example doesn't compile against 8.4, master, or against NickSdot:readonly-hooks > > Can you make this a script that runs and shows expected output so that readers don't have to assume this is supposed to do or run it?
Unfortunate typos in the RFC text. I can’t yet update the RFC text myself, but we will make sure they will be fixed shortly! Meanwhile, you can find the full running code for both examples here: NickSdot:readonly-hooks/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt NickSdot:readonly-hooks/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt Thanks for pointing these out! — I leave addressing the other points to Larry. Cheers, Nick

Larry Garfield

1 year ago
On Sun, Jun 8, 2025, at 7:17 AM, Volker Dusch wrote:
> Hey Larry, > > Couple points from a first read and from trying to run the examples. > > a) From the "ProductFromDB" i get: > >> Fatal error: Uncaught TypeError: LazyProduct::$category::get(): Return value must be of type Category, none returned in ... > > I assume you're missing a return statement here? Or is there something > I'm missing? > > b) Minor wording gripe in the proposal section > >> On the other hand, there is no shortage of dumb things that people can do with PHP already. > > While I don't disagree that PHP gives people a lot of freedom in how > they want to write their code, I find it a bit crude for an RFC to > phrase it like that. And the __get comparison is strong enough to stand > on its own.
Various typos have been fixed, and the code should be valid now. The wording has also been adapted (though I am pretty sure "silly" has appeared in RFCs before). I also fleshed out the __get mention with an example that shows what you can already do today, and in fact could since 8.1 when readonly was introduced. The hard guarantee of idempotency has never actually been there. (This also speaks to Claude's concern.)
> c) > >> That is, we feel, an entirely reasonable use of hooks, and would allow for lazy-load behavior per-property on readonly classes. > > I might be misunderstanding the sentence here, but on-demand/Lazy > initialization of properties on readonly classes is already possible in > classic getter/setter classes.
If a class uses private properties and getX/setX methods, sure, those methods can be overridden to do whatever you want. The whole point of hooks, though, is to NOT need those methods. We want to enable someone to define a read-model easily, like the Product in the example. That's all they should need to specify. So, yes, technically it's "in a class that is designed with modern features" rather than having a long list of getX/setX methods just to give lazy-loading generated code a place to hook in.
> f) Date: 2024-07-10 > > Is this correct? I know you created the page back then, but was there a > discussion already that I wasn't able to find?
Yes, this RFC was originally spun off from the hook-improvements RFC, as it needed more discussion while the other half of that RFC was uncontroversial. I've added a link to the prior thread for reference. --Larry Garfield

Tim Düsterhus

1 year ago
Hi Am 2025-06-09 17:11, schrieb Larry Garfield:
> I also fleshed out the __get mention with an example that shows what > you can already do today, and in fact could since 8.1 when readonly was > introduced. The hard guarantee of idempotency has never actually been > there. (This also speaks to Claude's concern.)
I don't think this really resolves Claude's concern. While it is certainly true that the guarantees do not currently hold, I don't believe this is strong enough of a reason not to provide for stronger guarantees in a *newly introduced feature*. The point of property hooks is for me that “dynamic properties” are easier to reason about compared to `__get()`. As a user when accessing a proper `readonly` property, I do not want to check if there is a property hook that might result in non-readonly behavior. As an engine developer I want to be able to optimize based on the `readonly`-ness of a property. Without such guarantees, the “readonly” keyword does not provide value to me. I also believe the LazyProduct example to be broken, since lazy-loading individual properties might result in an object that is internally consistent if the database changes in-between. Best regards Tim Düsterhus

Larry Garfield

1 year ago
On Tue, Jul 1, 2025, at 9:27 AM, Tim Düsterhus wrote:
> Hi > > Am 2025-06-09 17:11, schrieb Larry Garfield: >> I also fleshed out the __get mention with an example that shows what >> you can already do today, and in fact could since 8.1 when readonly was >> introduced. The hard guarantee of idempotency has never actually been >> there. (This also speaks to Claude's concern.) > > I don't think this really resolves Claude's concern. While it is > certainly true that the guarantees do not currently hold, I don't > believe this is strong enough of a reason not to provide for stronger > guarantees in a *newly introduced feature*. The point of property hooks > is for me that “dynamic properties” are easier to reason about compared > to `__get()`. As a user when accessing a proper `readonly` property, I > do not want to check if there is a property hook that might result in > non-readonly behavior. As an engine developer I want to be able to > optimize based on the `readonly`-ness of a property. Without such > guarantees, the “readonly” keyword does not provide value to me.
The only way to make the readonliness fully guaranteed would be to force a readonly property to be cached; (IE, the hook is only called at all if the property is uninitialized.) But there's no obvious way to make that clear in the code that it's what's happening.
> I also believe the LazyProduct example to be broken, since lazy-loading > individual properties might result in an object that is internally > consistent if the database changes in-between.
That's true with any lazy-loading scenario. The use of hooks doesn't change that at all. --Larry Garfield

Nicolas Grekas

1 year ago
Hi Larry et al. Le mar. 8 juil. 2025 à 17:12, Larry Garfield <larry@garfieldtech.com> a écrit :
> On Tue, Jul 1, 2025, at 9:27 AM, Tim Düsterhus wrote: > > Hi > > > > Am 2025-06-09 17:11, schrieb Larry Garfield: > >> I also fleshed out the __get mention with an example that shows what > >> you can already do today, and in fact could since 8.1 when readonly was > >> introduced. The hard guarantee of idempotency has never actually been > >> there. (This also speaks to Claude's concern.) > > > > I don't think this really resolves Claude's concern. While it is > > certainly true that the guarantees do not currently hold, I don't > > believe this is strong enough of a reason not to provide for stronger > > guarantees in a *newly introduced feature*. The point of property hooks > > is for me that “dynamic properties” are easier to reason about compared > > to `__get()`. As a user when accessing a proper `readonly` property, I > > do not want to check if there is a property hook that might result in > > non-readonly behavior. As an engine developer I want to be able to > > optimize based on the `readonly`-ness of a property. Without such > > guarantees, the “readonly” keyword does not provide value to me. > > The only way to make the readonliness fully guaranteed would be to force a > readonly property to be cached; (IE, the hook is only called at all if the > property is uninitialized.) But there's no obvious way to make that clear > in the code that it's what's happening. > > > I also believe the LazyProduct example to be broken, since lazy-loading > > individual properties might result in an object that is internally > > consistent if the database changes in-between. > > That's true with any lazy-loading scenario. The use of hooks doesn't > change that at all. >
This RFC makes sense to me. I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient. I also read Tim's argument that new features could be stricter. If one wants to be stricter and forbid extra behaviors that could be added by either the proposed hooks or __get, then the answer is : make the class final. This is the only real way to enforce readonly-ness in PHP. If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent. Nicolas PS: as I keep repeating, readonly doesn't immutable at all. I know this is written as such in the original RFC, but the concrete definition and implementation of readonly isn't: you can set mutable objects to readonly properties, and that means even readonly classes/properties are mutable, in the generic case.

Claude Pache

1 year ago
> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit : > > I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient. >
It is true that readonly could be bypassed by __get(); but this is a legacy behaviour, and you have to take an explicit step to make it possible. For those unaware of the awful hack, here is a minimal test case: https://3v4l.org/N78An where the `unset(...)` is mandatory to make it “work”. Are we obligated to sanction shortcomings of legacy concepts in newly introduced concepts that are supposed to replace them? Or can we do something better? I’ve outlined in a previous email what I think is a better design for such situation (namely an `init` hook). Also, the fact that __get() is not yet deprecated means that we can still use the aforementioned hack until/unless we’ve implemented a proper solution. In the worst case, you can still use a non-readonly hooked property and document the intended invariants in phpdoc.
> If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent. >
Enforcing as strictly as possible its intended invariants is a good design for a robust language. Yes, it implies that users cannot (or can hardly) escape annoying constraints. For example, you can’t extend a final class, even if you think that you have good reason for it, like constructing a mock object. —Claude

Nicolas Grekas

1 year ago
Thanks for your detailed thoughts, Claude. I'd like to offer my perspective on some of the points you raised. Le mer. 9 juil. 2025 à 12:53, Claude Pache <claude.pache@gmail.com> a écrit :
> > > Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a > écrit : > > I read Claude's concern, and I agree with Larry's response: the engine > already allows readonly to be bypassed using __get. The added hook doesn't > make anything more lenient. > > > It is true that readonly could be bypassed by __get(); but this is a > legacy behaviour, and you have to take an explicit step to make it > possible. For those unaware of the awful hack, here is a minimal test case: > > https://3v4l.org/N78An > > where the `unset(...)` is mandatory to make it “work”. > > Are we obligated to sanction shortcomings of legacy concepts in newly > introduced concepts that are supposed to replace them? Or can we do > something better? I’ve outlined in a previous email what I think is a > better design for such situation (namely an `init` hook). > > Also, the fact that __get() is not yet deprecated means that we can still > use the aforementioned hack until/unless we’ve implemented a proper > solution. In the worst case, you can still use a non-readonly hooked > property and document the intended invariants in phpdoc. >
__get is certainly not legacy; removing it would break many use cases without proper alternatives. The behavior after unset() has been promoted to a language feature when readonly properties were introduced *because* it helps solve real world use cases. I've been asked recently by Gina if those use cases were covered by eg native lazy proxies. The answer is *no*, because native lazy proxies cover only part of the lazy-proxying domain: what remains is proxying by interface and proxying internal classes, and those require a way to proxy all property accesses, which is why magic methods are required. With the argument that __get can be used to implement the non-readonly-ness, we could also say that hooks are not needed, because they can be implemented using __get. Yet, language aesthetics are important, and we welcomed hooks for this reason. Being able to easily lazy-init thanks to hooks on readonly would be a welcome improvement to me. That being said, about your init proposal, I think that could work. I'd just do it a bit differently: instead of introducing a new "init" hook, I'd prefer having "set" mean "init" for readonly properties. But I know nothing about the engine on the topic so I can't comment on the feasibility aspect. I'll leave this to others. Just a word about using hooks vs __get for lazy-init: the really hard part when using __get is emulating the public/protected/private visibility rules. Hooks make this a non-issue. Yet hooks - unfortunately - can't be used as a generic lazy-init implementation because of their behavior related to references. That's another topic, but still related, to reinforce that __get is certainly not legacy.
> > If a class is final and uses readonly with either hooks or __get, then > that's the original author's choice. There's no need for extra > engine-assisted strictness in this case. You cannot write such code in a > non-readonly way by mistake, so it has to be by intent. > > > Enforcing as strictly as possible its intended invariants is a good design > for a robust language. Yes, it implies that users cannot (or can hardly) > escape annoying constraints. For example, you can’t extend a final class, > even if you think that you have good reason for it, like constructing a > mock object. >
That's not strictness when the root concept is already filled with conceptual holes... I'm surprised nobody ever proposed the concept of an *immutable* keyword, that'd be like readonly but that'd accept only also-immutable values. Until this happens, using readonly for that is a fallacy I'm sorry... To me that invalidates all related arguments. Nicolas

Rob Landers

1 year ago
On Wed, Jul 9, 2025, at 13:39, Nicolas Grekas wrote:
> Thanks for your detailed thoughts, Claude. I'd like to offer my perspective on some of the points you raised. > > Le mer. 9 juil. 2025 à 12:53, Claude Pache <claude.pache@gmail.com> a écrit : >> >> >>> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com <mailto:nicolas.grekas%2Bphp@gmail.com>> a écrit : >>> >>> I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient. >>> >> >> It is true that readonly could be bypassed by __get(); but this is a legacy behaviour, and you have to take an explicit step to make it possible. For those unaware of the awful hack, here is a minimal test case: >> >> https://3v4l.org/N78An >> >> where the `unset(...)` is mandatory to make it “work”. >> >> Are we obligated to sanction shortcomings of legacy concepts in newly introduced concepts that are supposed to replace them? Or can we do something better? I’ve outlined in a previous email what I think is a better design for such situation (namely an `init` hook). >> >> Also, the fact that __get() is not yet deprecated means that we can still use the aforementioned hack until/unless we’ve implemented a proper solution. In the worst case, you can still use a non-readonly hooked property and document the intended invariants in phpdoc. > > > __get is certainly not legacy; removing it would break many use cases without proper alternatives. > The behavior after unset() has been promoted to a language feature when readonly properties were introduced *because* it helps solve real world use cases. > I've been asked recently by Gina if those use cases were covered by eg native lazy proxies. The answer is *no*, because native lazy proxies cover only part of the lazy-proxying domain: what remains is proxying by interface and proxying internal classes, and those require a way to proxy all property accesses, which is why magic methods are required. > > With the argument that __get can be used to implement the non-readonly-ness, we could also say that hooks are not needed, because they can be implemented using __get. Yet, language aesthetics are important, and we welcomed hooks for this reason. Being able to easily lazy-init thanks to hooks on readonly would be a welcome improvement to me. > > That being said, about your init proposal, I think that could work. I'd just do it a bit differently: instead of introducing a new "init" hook, I'd prefer having "set" mean "init" for readonly properties. But I know nothing about the engine on the topic so I can't comment on the feasibility aspect. I'll leave this to others. > > Just a word about using hooks vs __get for lazy-init: the really hard part when using __get is emulating the public/protected/private visibility rules. Hooks make this a non-issue. Yet hooks - unfortunately - can't be used as a generic lazy-init implementation because of their behavior related to references. That's another topic, but still related, to reinforce that __get is certainly not legacy. > > >> >> >> >>> If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent. >>> >> >> Enforcing as strictly as possible its intended invariants is a good design for a robust language. Yes, it implies that users cannot (or can hardly) escape annoying constraints. For example, you can’t extend a final class, even if you think that you have good reason for it, like constructing a mock object. > > > That's not strictness when the root concept is already filled with conceptual holes... I'm surprised nobody ever proposed the concept of an *immutable* keyword, that'd be like readonly but that'd accept only also-immutable values. Until this happens, using readonly for that is a fallacy I'm sorry... To me that invalidates all related arguments. > > Nicolas
https://wiki.php.net/rfc/records I’ll probably return back to it after 8.5 is released. Knowing what I know today, there are a lot of things id remove. — Rob

Nick

1 year ago
Hey Claude,
>> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit : >> >> I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient. >> > > It is true that readonly could be bypassed by __get(); but this is a legacy behaviour, and you have to take an explicit step to make it possible. For those unaware of the awful hack, here is a minimal test case: > > https://3v4l.org/N78An > > where the `unset(...)` is mandatory to make it “work”. > > Are we obligated to sanction shortcomings of legacy concepts in newly introduced concepts that are supposed to replace them? Or can we do something better? I’ve outlined in a previous email what I think is a better design for such situation (namely an `init` hook). > > Also, the fact that __get() is not yet deprecated means that we can still use the aforementioned hack until/unless we’ve implemented a proper solution. In the worst case, you can still use a non-readonly hooked property and document the intended invariants in phpdoc. > > >> If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent. >> > > Enforcing as strictly as possible its intended invariants is a good design for a robust language. Yes, it implies that users cannot (or can hardly) escape annoying constraints. For example, you can’t extend a final class, even if you think that you have good reason for it, like constructing a mock object. > > —Claude
I hear you, but I still struggle to fully grasp the issue. It’s genuinely hard for me to come up with a real-world example that actually makes sense. Everything I’ve seen so far, including the RFC example and what I tried myself (I gave it an honest shot), feels either very theoretical or entirely intentional, and thus perfectly logical in its outcome. In one of your previous mails you brought up an example that requires calling a class method (read: intentionally changing class state), which would result in a non-consistent value being returned when calling the same property more than once. I get it. But what if the user wants exactly that in their `readonly` class? That said I did address your concern here (actual RFC PR branch against alternative; PoC): https://github.com/NickSdot/php-php-src/compare/allow-readonly-hooks...NickSdot:php-php-src:readonly-hooks-strict Larry and I agree that we don’t want this complexity in the current RFC. Perhaps this is something for a separate `init` hook RFC? Cheers, Nick (Sorry for the duplicate. I forgot to CC the list)

Claude Pache

1 year ago
> Le 9 juil. 2025 à 15:17, Nick <php@nicksdot.dev> a écrit : > > Hey Claude, > > > I hear you, but I still struggle to fully grasp the issue. It’s genuinely hard for me to come up with a real-world example that actually makes sense. > Everything I’ve seen so far, including the RFC example and what I tried myself (I gave it an honest shot), feels either very theoretical or entirely intentional, and thus perfectly logical in its outcome. > > In one of your previous mails you brought up an example that requires calling a class method (read: intentionally changing class state), which would result in a non-consistent value being returned when calling the same property more than once. I get it. But what if the user wants exactly that in their `readonly` class?
Yes, it’s mostly theoretical, but it is good to base language design on sound theory. But here is a potential practical issue. A random user wants to extend a class from a third-party library, but they are annoyed that a given property is readonly. Now, using a get hook, it is trivial for them to cheat and to work around what it perceives as an undue limitation, not realising that it may break assumptions made elsewhere in the library. — Indeed, I don’t trust users and want to protect them against themselves.
> > That said I did address your concern here (actual RFC PR branch against alternative; PoC): > https://github.com/NickSdot/php-php-src/compare/allow-readonly-hooks...NickSdot:php-php-src:readonly-hooks-strict > > Larry and I agree that we don’t want this complexity in the current RFC. > Perhaps this is something for a separate `init` hook RFC?
I think indeed that it is not worth making the current proposal more complex, but rather considering whether implementing an init hook instead is a reasonable alternative. Also there is another issue with the use of get hook for lazy initialisation (although not specific to readonly): The `??=` pattern breaks if the property is nullable and you initialise it to `null`. It is in fact cumbersome to distinguish between an uninitialised property and a property initialised with null. —Claude

Tim Düsterhus

1 year ago
Hi On 7/9/25 19:58, Claude Pache wrote:
>> I hear you, but I still struggle to fully grasp the issue. It’s genuinely hard for me to come up with a real-world example that actually makes sense. >> Everything I’ve seen so far, including the RFC example and what I tried myself (I gave it an honest shot), feels either very theoretical or entirely intentional, and thus perfectly logical in its outcome. >> >> In one of your previous mails you brought up an example that requires calling a class method (read: intentionally changing class state), which would result in a non-consistent value being returned when calling the same property more than once. I get it. But what if the user wants exactly that in their `readonly` class? > > Yes, it’s mostly theoretical, but it is good to base language design on sound theory. > > But here is a potential practical issue. A random user wants to extend a class from a third-party library, but they are annoyed that a given property is readonly. Now, using a get hook, it is trivial for them to cheat and to work around what it perceives as an undue limitation, not realising that it may break assumptions made elsewhere in the library. — Indeed, I don’t trust users and want to protect them against themselves. >
Full agreement on Claude's entire email, but particularly this part. Users have expectations from seeing the `readonly` keyword and adding the `readonly` keyword is an intentional choice by the class author. The language should not allow making it easy to violate these expectations (by accident). This is no different from the language making sure for you that you may only return values of an appropriate type from a function having a return type. The `readonly` keyword is part of your public API just like the types are. Best regards Tim Düsterhus

Larry Garfield

1 year ago
On Wed, Jul 9, 2025, at 5:52 AM, Claude Pache wrote:
>> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit : >> >> I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient. >> > > It is true that readonly could be bypassed by __get(); but this is a > legacy behaviour, and you have to take an explicit step to make it > possible. For those unaware of the awful hack, here is a minimal test > case: > > https://3v4l.org/N78An > > where the `unset(...)` is mandatory to make it “work”. > > Are we obligated to sanction shortcomings of legacy concepts in newly > introduced concepts that are supposed to replace them? Or can we do > something better? I’ve outlined in a previous email what I think is a > better design for such situation (namely an `init` hook). > > Also, the fact that __get() is not yet deprecated means that we can > still use the aforementioned hack until/unless we’ve implemented a > proper solution. In the worst case, you can still use a non-readonly > hooked property and document the intended invariants in phpdoc. > > >> If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent. >> > > Enforcing as strictly as possible its intended invariants is a good > design for a robust language. Yes, it implies that users cannot (or can > hardly) escape annoying constraints. For example, you can’t extend a > final class, even if you think that you have good reason for it, like > constructing a mock object. > > —Claude
Here's the core problem right now: 1. `readonly` bills itself as immutability, but it fundamentally is not. There are at least two loopholes: __get and a mutable object saved to a property. So while it offering immutability guarantees is nice in theory, it's simply not true in practice. `readonly` has always been misnamed; it should really be `writeonce`, because that's all it is. (Once again, this is likely the most poorly designed feature we've added in many years.) 2. In 8.4, if a class is marked `readonly`, you basically forbid it from having any hooks of any kind, even though you absolutely can honor the write-once-ness of the properties while still having hooks. And that applies to child classes, too, because `readonly`-ness inherits. So adding a single hook means you have to move the readonly to all the other properties individually, which if inheritance is involved you cannot do. The RFC aims to address point 2 in a way that still respects point 1, but only point 1 as it actually is (write-once), not as we wish it to be (immutability). In practice, there's 2 scenarios that I see as useful (or problematic in 8.4, that we want to support): * set hooks for validation, which don't impact writeonce-ness. I think everyone seems on board with that. * Lazy computed properties. I use these a ton, even for internal caching purposes. 99% of the time I cache them because my objects are practically immutable, and $this->foo ??= whatever is an easy enough pattern. (If they're not cached then it would be a virtual property, which we're not dealing with for now.) As long as you're caching it in that fashion, the write-once-ness still ends up respected. Honestly, Nick tried to come up with examples yesterday while we were talking that would not fit into one of those two categories, and for every one of them my answer was "if your code is already that badly designed, there's nothing we can do for you." :-) Ilija and I had discussed making `readonly` imply cached/lazy/init in the original hooks RFC, but decided against it. Mainly, it becomes very confusing if a property is going to store a value, as there's three different scenarios to consider: There's a short-set hook, the property is mentioned in its own hooks, and then look for readonly. (Would that mean readonly only works on virtual properties?) It makes a feature that's already, in all honesty, at the edge of reasonable complexity more complex. An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???" I'd be open to a follow up RFC for an init hook, though I likely wouldn't write it myself. But that's a different topic than what we're addressing here. --Larry Garfield

Eric Norris

1 year ago
> An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???" > > I'd be open to a follow up RFC for an init hook, though I likely wouldn't write it myself. But that's a different topic than what we're addressing here. > > --Larry Garfield
I'm not entirely sure I follow - it sounds like your email states that `readonly` should be interpreted as `writeonce`, which makes sense, but then why would an `init` hook not be the appropriate answer here? The two scenarios you listed (`set` hooks for validation and lazy computed properties) seem like they could be solved by allowing `set` hooks (everyone seems +1 to that), an `init` hook, and disallowing `get` hooks. It would sidestep the controversial nature of a `get` hook for the property. It feels to me like an init hook would be the more conservative approach, and would (I imagine) still allow for potential `readonly` engine optimizations like Tim pointed out. Once we allow `get` hooks, there's no going back. If we still needed to add `get` hooks in the future, it's not off the table. I don't know that I feel strongly here, but there does seem something intuitively off with allowing a get hook for a readonly (writeonce) property.

Larry Garfield

1 year ago
On Wed, Jul 9, 2025, at 10:42 AM, Eric Norris wrote:
>> An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???" >> >> I'd be open to a follow up RFC for an init hook, though I likely wouldn't write it myself. But that's a different topic than what we're addressing here. >> >> --Larry Garfield > > I'm not entirely sure I follow - it sounds like your email states that > `readonly` should be interpreted as `writeonce`, which makes sense, > but then why would an `init` hook not be the appropriate answer here? > > The two scenarios you listed (`set` hooks for validation and lazy > computed properties) seem like they could be solved by allowing `set` > hooks (everyone seems +1 to that), an `init` hook, and disallowing > `get` hooks. It would sidestep the controversial nature of a `get` > hook for the property. > > It feels to me like an init hook would be the more conservative > approach, and would (I imagine) still allow for potential `readonly` > engine optimizations like Tim pointed out. Once we allow `get` hooks, > there's no going back. If we still needed to add `get` hooks in the > future, it's not off the table. > > I don't know that I feel strongly here, but there does seem something > intuitively off with allowing a get hook for a readonly (writeonce) > property.
Can an init hook reference itself, the way get and set can? If there is both an init and set hook, what happens? Is it different if set reads from itself than if it writes to itself? Should combining init and set be forbidden as confusing? Can you have both an init hook and a get hook? What happens then? Repeat all of the above on readonly properties. I don't know the answer to any of those. We could probably collectively figure out some answers to that in time, but that's a much larger lift than either Nick or I have any interest in engaging in at this point, especially when there is a reasonable solution right in front of us that is trivial to implement. --Larry Garfield

Tim Düsterhus

1 year ago
Hi On 7/9/25 16:05, Larry Garfield wrote:
> 1. `readonly` bills itself as immutability, but it fundamentally is not. There are at least two loopholes: __get and a mutable object saved to a property. So while it offering immutability guarantees is nice in theory, it's simply not true in practice. `readonly` has always been misnamed; it should really be `writeonce`, because that's all it is. (Once again, this is likely the most poorly designed feature we've added in many years.)
No, readonly is readonly, not writeonce. Stop trying to redefine readonly as writeonce to justify bad design decisions. Readonly guarantees that once I successfully read from a property that I'll get the same thing out on subsequent reads and I consider this to be valuable and strongly disagree on the "most poorly designed feature" bit. Yes, I understand that __get() currently is an exception to that guarantee, but that does not mean that further exceptions should be added to water down readonly into something that is completely useless.
> 2. In 8.4, if a class is marked `readonly`, you basically forbid it from having any hooks of any kind, even though you absolutely can honor the write-once-ness of the properties while still having hooks. And that applies to child classes, too, because `readonly`-ness inherits. So adding a single hook means you have to move the readonly to all the other properties individually, which if inheritance is involved you cannot do. > > The RFC aims to address point 2 in a way that still respects point 1, but only point 1 as it actually is (write-once), not as we wish it to be (immutability).
Readonly is immutability of values (or in other words immutability of identity). For objects this means immutability of the object handle, for other types this means actual immutability. I also feel compelled to mention at this point that the commonly repeated statement of "Objects are passed by reference" is incorrect. It's that "the object handle is passed by value". And then it's fully consistent with how readonly works as of now.
> * set hooks for validation, which don't impact writeonce-ness. I think everyone seems on board with that.
Yes, allowing set hooks for readonly properties seems sound to me.
> * Lazy computed properties. I use these a ton, even for internal caching purposes. 99% of the time I cache them because my objects are practically immutable, and $this->foo ??= whatever is an easy enough pattern. (If they're not cached then it would be a virtual property, which we're not dealing with for now.) As long as you're caching it in that fashion, the write-once-ness still ends up respected. > > Honestly, Nick tried to come up with examples yesterday while we were talking that would not fit into one of those two categories, and for every one of them my answer was "if your code is already that badly designed, there's nothing we can do for you." :-)
It's nice to hear that there are no other usecases for hooks on readonly properties, since this means that we can just allow the 'set' hook and add an 'init' hook for the lazy computation use-case without needing to violate the semantics of `readonly` by allowing a `get` hook.
> An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???"
Not clearer. It would be the only thing that is semantically sound. While it certainly needs careful consideration of semantics to ensure there are no edge cases, figuring this out should be much easier than intentionally introducing edge cases via a get hook. As to your questions: The init hook is triggered when reading from a property that is in the uninitialized state. The return value of the hook is stored in the property and returned as the result of the read operation. Having an init hook implies the property is non-virtual. - Yes, you can set something that has an init hook. Setting means that the property will no longer be uninitialized, which means that the init hook will no longer be called. - If there is both a get and an init hook, the init hook will be called when the backing store is uninitialized. The result of the init hook will then also go through the get hook. On subsequent reads only the get hook will be called. Best regards Tim Düsterhus

Faizan Akram Dar

1 year ago
On Fri, 18 Jul 2025, 15:16 Tim Düsterhus, <tim@bastelstu.be> wrote:
> Hi > > On 7/9/25 16:05, Larry Garfield wrote: > > 1. `readonly` bills itself as immutability, but it fundamentally is > not. There are at least two loopholes: __get and a mutable object saved to > a property. So while it offering immutability guarantees is nice in > theory, it's simply not true in practice. `readonly` has always been > misnamed; it should really be `writeonce`, because that's all it is. (Once > again, this is likely the most poorly designed feature we've added in many > years.) > > No, readonly is readonly, not writeonce. Stop trying to redefine > readonly as writeonce to justify bad design decisions. > > Readonly guarantees that once I successfully read from a property that > I'll get the same thing out on subsequent reads and I consider this to > be valuable and strongly disagree on the "most poorly designed feature" > bit. > > Yes, I understand that __get() currently is an exception to that > guarantee, but that does not mean that further exceptions should be > added to water down readonly into something that is completely useless. > > > 2. In 8.4, if a class is marked `readonly`, you basically forbid it from > having any hooks of any kind, even though you absolutely can honor the > write-once-ness of the properties while still having hooks. And that > applies to child classes, too, because `readonly`-ness inherits. So adding > a single hook means you have to move the readonly to all the other > properties individually, which if inheritance is involved you cannot do. > > > > The RFC aims to address point 2 in a way that still respects point 1, > but only point 1 as it actually is (write-once), not as we wish it to be > (immutability). > > Readonly is immutability of values (or in other words immutability of > identity). For objects this means immutability of the object handle, for > other types this means actual immutability. > > I also feel compelled to mention at this point that the commonly > repeated statement of "Objects are passed by reference" is incorrect. > It's that "the object handle is passed by value". And then it's fully > consistent with how readonly works as of now. > > > * set hooks for validation, which don't impact writeonce-ness. I think > everyone seems on board with that. > > Yes, allowing set hooks for readonly properties seems sound to me. > > > * Lazy computed properties. I use these a ton, even for internal > caching purposes. 99% of the time I cache them because my objects are > practically immutable, and $this->foo ??= whatever is an easy enough > pattern. (If they're not cached then it would be a virtual property, which > we're not dealing with for now.) As long as you're caching it in that > fashion, the write-once-ness still ends up respected. > > > > Honestly, Nick tried to come up with examples yesterday while we were > talking that would not fit into one of those two categories, and for every > one of them my answer was "if your code is already that badly designed, > there's nothing we can do for you." :-) > > It's nice to hear that there are no other usecases for hooks on readonly > properties, since this means that we can just allow the 'set' hook and > add an 'init' hook for the lazy computation use-case without needing to > violate the semantics of `readonly` by allowing a `get` hook. > > > An init hook would be clearer, certainly, though it also has its own > edge cases. Can you set something that has an init hook? What happens if > there's both a get and init hook? These probably have answers that could > be sorted out, but that's a different question from "why the <censored> > does a readonly class forbid me using even rudimentary hooks???" > > Not clearer. It would be the only thing that is semantically sound. > While it certainly needs careful consideration of semantics to ensure > there are no edge cases, figuring this out should be much easier than > intentionally introducing edge cases via a get hook. > > As to your questions: The init hook is triggered when reading from a > property that is in the uninitialized state. The return value of the > hook is stored in the property and returned as the result of the read > operation. Having an init hook implies the property is non-virtual. > > - Yes, you can set something that has an init hook. Setting means that > the property will no longer be uninitialized, which means that the init > hook will no longer be called. > - If there is both a get and an init hook, the init hook will be called > when the backing store is uninitialized. The result of the init hook > will then also go through the get hook. On subsequent reads only the get > hook will be called. > > Best regards > Tim Düsterhus >
Hi Tim, The problem with allowing only set hooks is that readonly class won't be compatible with hooks, I think that is one of the main motivations behind this RFC. Faizan Akram Dar faizanakram.me

Tim Düsterhus

1 year ago
Hi Am 2025-07-08 17:32, schrieb Nicolas Grekas:
> I also read Tim's argument that new features could be stricter. If one > wants to be stricter and forbid extra behaviors that could be added by > either the proposed hooks or __get, then the answer is : make the class > final. This is the only real way to enforce readonly-ness in PHP.
Making the class final still would not allow to optimize based on the fact that the identity of a value stored in a readonly property will not change after successfully reading from the property once. Whether or not a property hooked must be considered an implementation detail, since a main point of the property hooks RFC was that hooks can be added and removed without breaking compatibility for the user of the API.
> engine-assisted strictness in this case. You cannot write such code in > a > non-readonly way by mistake, so it has to be by intent.
That is saying "it's impossible to introduce bugs".
> PS: as I keep repeating, readonly doesn't immutable at all. I know this > is > written as such in the original RFC, but the concrete definition and > implementation of readonly isn't: you can set mutable objects to > readonly > properties, and that means even readonly classes/properties are > mutable, in > the generic case.
`readonly` guarantees the immutability of identity. While you can certainly mutate mutable objects, the identity of the stored object doesn't change. Best regards Tim Düsterhus

Larry Garfield

1 year ago
On Thu, Jul 10, 2025, at 5:43 AM, Tim Düsterhus wrote:
> Hi > > Am 2025-07-08 17:32, schrieb Nicolas Grekas: >> I also read Tim's argument that new features could be stricter. If one >> wants to be stricter and forbid extra behaviors that could be added by >> either the proposed hooks or __get, then the answer is : make the class >> final. This is the only real way to enforce readonly-ness in PHP. > > Making the class final still would not allow to optimize based on the > fact that the identity of a value stored in a readonly property will not > change after successfully reading from the property once. Whether or not > a property hooked must be considered an implementation detail, since a > main point of the property hooks RFC was that hooks can be added and > removed without breaking compatibility for the user of the API. > >> engine-assisted strictness in this case. You cannot write such code in >> a >> non-readonly way by mistake, so it has to be by intent. > > That is saying "it's impossible to introduce bugs". > >> PS: as I keep repeating, readonly doesn't immutable at all. I know this >> is >> written as such in the original RFC, but the concrete definition and >> implementation of readonly isn't: you can set mutable objects to >> readonly >> properties, and that means even readonly classes/properties are >> mutable, in >> the generic case. > > `readonly` guarantees the immutability of identity. While you can > certainly mutate mutable objects, the identity of the stored object > doesn't change. > > Best regards > Tim Düsterhus
Nick previously suggested having the get-hook's first return value cached; it would still be subsequently called, so any side effects would still happen (though I don't know why you'd want side effects), but only the first returned value would ever get returned. Would anyone find that acceptable? (In the typical case, it would be the same as the current $this->foo ??= compute() pattern, just with an extra cache entry.) --Larry Garfield

Rob Landers

1 year ago
On Thu, Jul 10, 2025, at 17:34, Larry Garfield wrote:
> On Thu, Jul 10, 2025, at 5:43 AM, Tim Düsterhus wrote: > > Hi > > > > Am 2025-07-08 17:32, schrieb Nicolas Grekas: > >> I also read Tim's argument that new features could be stricter. If one > >> wants to be stricter and forbid extra behaviors that could be added by > >> either the proposed hooks or __get, then the answer is : make the class > >> final. This is the only real way to enforce readonly-ness in PHP. > > > > Making the class final still would not allow to optimize based on the > > fact that the identity of a value stored in a readonly property will not > > change after successfully reading from the property once. Whether or not > > a property hooked must be considered an implementation detail, since a > > main point of the property hooks RFC was that hooks can be added and > > removed without breaking compatibility for the user of the API. > > > >> engine-assisted strictness in this case. You cannot write such code in > >> a > >> non-readonly way by mistake, so it has to be by intent. > > > > That is saying "it's impossible to introduce bugs". > > > >> PS: as I keep repeating, readonly doesn't immutable at all. I know this > >> is > >> written as such in the original RFC, but the concrete definition and > >> implementation of readonly isn't: you can set mutable objects to > >> readonly > >> properties, and that means even readonly classes/properties are > >> mutable, in > >> the generic case. > > > > `readonly` guarantees the immutability of identity. While you can > > certainly mutate mutable objects, the identity of the stored object > > doesn't change. > > > > Best regards > > Tim Düsterhus > > Nick previously suggested having the get-hook's first return value cached; it would still be subsequently called, so any side effects would still happen (though I don't know why you'd want side effects), but only the first returned value would ever get returned. Would anyone find that acceptable? (In the typical case, it would be the same as the current $this->foo ??= compute() pattern, just with an extra cache entry.) > > --Larry Garfield >
I think that only covers one use-case for getters on readonly classes. Take this example for discussion: readonly class User { public int $elapsedTimeSinceCreation { get => time() - $this->createdAt; } private int $cachedResult; public int $totalBalance { get => $this->cachedResult ??= 5+10; } public int $accessLevel { get => getCurrentAccessLevel(); } public function __construct(public int $createdAt) {} } $user = new User(time() - 5); var_dump($user->elapsedTimeSinceCreation); // 5 var_dump($user->totalBalance); // 15 var_dump($user->accessLevel); // 42 In this example, we have three of the most common ones: 1. Computed Properties (elapsedTimeSinceCreation): these are properties of the object that are relevant to the object in question, but are not static. In this case, you are not writing to the object. It is still "readonly". 2. Memoization (expensiveCalculation): only calculate the property once and only once. This is a performance optimization. It is still "readonly". 3. External State (accessLevel): properties of the object that rely on some external state, which due to architecture or other convienence may not make sense as part of object construction. It is still "readonly". You can mix-and-match these to provide your own level of immutability, but memoization is certainly not the only one. You could make the argument that these should be functions, but I'd posit that these are properties of the user object. In other words, a function to get these values would probably be named `getElapsedTimeSinceCreation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway. — Rob

Nick

1 year ago
Hey Rob,
> On 11. Jul 2025, at 01:43, Rob Landers <rob@bottled.codes> wrote: >> >> Nick previously suggested having the get-hook's first return value cached; it would still be subsequently called, so any side effects would still happen (though I don't know why you'd want side effects), but only the first returned value would ever get returned. Would anyone find that acceptable? (In the typical case, it would be the same as the current $this->foo ??= compute() pattern, just with an extra cache entry.) >> >> --Larry Garfield >> > > I think that only covers one use-case for getters on readonly classes. Take this example for discussion: > > readonly class User { > public int $elapsedTimeSinceCreation { get => time() - $this->createdAt; } > private int $cachedResult; > public int $totalBalance { get => $this->cachedResult ??= 5+10; } > public int $accessLevel { get => getCurrentAccessLevel(); } > public function __construct(public int $createdAt) {} > } > > $user = new User(time() - 5); > var_dump($user->elapsedTimeSinceCreation); // 5 > var_dump($user->totalBalance); // 15 > var_dump($user->accessLevel); // 42 > > In this example, we have three of the most common ones: > Computed Properties (elapsedTimeSinceCreation): these are properties of the object that are relevant to the object in question, but are not static. In this case, you are not writing to the object. It is still "readonly". > Memoization (expensiveCalculation): only calculate the property once and only once. This is a performance optimization. It is still "readonly". > External State (accessLevel): properties of the object that rely on some external state, which due to architecture or other convienence may not make sense as part of object construction. It is still "readonly". > You can mix-and-match these to provide your own level of immutability, but memoization is certainly not the only one. > > You could make the argument that these should be functions, but I'd posit that these are properties of the user object. In other words, a function to get these values would probably be named `getElapsedTimeSinceCreation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway. > > — Rob
Please remember that the RFC will allow `readonly` on backed properties only, not on virtual hooked properties. Nothing from your example would work, and it would result in: Fatal error: Hooked virtual properties cannot be declared readonly My proposed alternative implementation with caching addresses the concern Claude and Tim had and will make this hold: ```php class Unusual { public function __construct( public readonly int $value { get => $this->value * random_int(1, 100); } ) {} } $unusual = new Unusual(1); var_dump($unusual->value === $unusual->value); // true ``` – Nick

Nick

1 year ago
> On 11. Jul 2025, at 01:43, Rob Landers <rob@bottled.codes> wrote: > > On Thu, Jul 10, 2025, at 17:34, Larry Garfield wrote: >> On Thu, Jul 10, 2025, at 5:43 AM, Tim Düsterhus wrote: >> > Hi >> > >> > Am 2025-07-08 17:32, schrieb Nicolas Grekas: >> >> I also read Tim's argument that new features could be stricter. If one >> >> wants to be stricter and forbid extra behaviors that could be added by >> >> either the proposed hooks or __get, then the answer is : make the class >> >> final. This is the only real way to enforce readonly-ness in PHP. >> > >> > Making the class final still would not allow to optimize based on the >> > fact that the identity of a value stored in a readonly property will not >> > change after successfully reading from the property once. Whether or not >> > a property hooked must be considered an implementation detail, since a >> > main point of the property hooks RFC was that hooks can be added and >> > removed without breaking compatibility for the user of the API. >> > >> >> engine-assisted strictness in this case. You cannot write such code in >> >> a >> >> non-readonly way by mistake, so it has to be by intent. >> > >> > That is saying "it's impossible to introduce bugs". >> > >> >> PS: as I keep repeating, readonly doesn't immutable at all. I know this >> >> is >> >> written as such in the original RFC, but the concrete definition and >> >> implementation of readonly isn't: you can set mutable objects to >> >> readonly >> >> properties, and that means even readonly classes/properties are >> >> mutable, in >> >> the generic case. >> > >> > `readonly` guarantees the immutability of identity. While you can >> > certainly mutate mutable objects, the identity of the stored object >> > doesn't change. >> > >> > Best regards >> > Tim Düsterhus >> >> Nick previously suggested having the get-hook's first return value cached; it would still be subsequently called, so any side effects would still happen (though I don't know why you'd want side effects), but only the first returned value would ever get returned. Would anyone find that acceptable? (In the typical case, it would be the same as the current $this->foo ??= compute() pattern, just with an extra cache entry.) >> >> --Larry Garfield >> > > I think that only covers one use-case for getters on readonly classes. Take this example for discussion: > > readonly class User { > public int $elapsedTimeSinceCreation { get => time() - $this->createdAt; } > private int $cachedResult; > public int $totalBalance { get => $this->cachedResult ??= 5+10; } > public int $accessLevel { get => getCurrentAccessLevel(); } > public function __construct(public int $createdAt) {} > } > > $user = new User(time() - 5); > var_dump($user->elapsedTimeSinceCreation); // 5 > var_dump($user->totalBalance); // 15 > var_dump($user->accessLevel); // 42 > > In this example, we have three of the most common ones: > Computed Properties (elapsedTimeSinceCreation): these are properties of the object that are relevant to the object in question, but are not static. In this case, you are not writing to the object. It is still "readonly". > Memoization (expensiveCalculation): only calculate the property once and only once. This is a performance optimization. It is still "readonly". > External State (accessLevel): properties of the object that rely on some external state, which due to architecture or other convienence may not make sense as part of object construction. It is still "readonly". > You can mix-and-match these to provide your own level of immutability, but memoization is certainly not the only one. > > You could make the argument that these should be functions, but I'd posit that these are properties of the user object. In other words, a function to get these values would probably be named `getElapsedTimeSinceCreation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway. > > — Rob
Hey Rob, We ended up where we are now because more people than not voiced that they would expect a `readonly` property value to never change after `get` was first called. As you can see in my earlier mails I also was of a different opinion. I asked "what if a user wants exactly that”? You brought good examples for when “that" could be the case. It is correct, with the current alternative implementations your examples would be cached. A later call to the property would *not* use the updated time or a potentially updated external state. After thinking a lot about it over the last days I think that makes sense. To stick to your usage of `time()`, I think the following is a good example: ```php readonly class JobHelper { public function __construct( public readonly string $uniqueRunnerKey { get => 'runner/' . date("Ymd_H-i-s", time()) . '_' . (string) random_int(1, 100) . '/'. $this->uniqueRunnerKey; } ) {} } $helper = new JobHelper('report.txt’); $key1 = $helper->uniqueRunnerKey; sleep(2); $key2 = $helper->uniqueRunnerKey; var_dump($key1 === $key2); // true ``` It has two dynamic path elements, to achieve some kind of randomness. As a user you still can expect $key1 === $key2 to hold when using `readonly`. Claude's argument is strong, because we also cannot write twice to a `readonly` property. So it’s fair to say reading should also be predictable, and return the exact same value on consecutive calls. If users don’t want that, they can opt-out by not using `readonly`. The guarantee only holds in combination with `readonly`. Alternatively, as you proposed, using methods (which I think would really be a better fit; alternatively virtual properties which also will not support `readonly`. With what we have now, both “camps" will be able to achieve what they want transparently. And I believe that’s a good middle ground we should go forward with. Cheers, Nick

Tim Düsterhus

1 year ago
Hi On 7/10/25 17:34, Larry Garfield wrote:
> Nick previously suggested having the get-hook's first return value cached; it would still be subsequently called, so any side effects would still happen (though I don't know why you'd want side effects), but only the first returned value would ever get returned. Would anyone find that acceptable? (In the typical case, it would be the same as the current $this->foo ??= compute() pattern, just with an extra cache entry.)
I'm seeing this proposal has already been dropped, but to spell it out explicitly: No, I would not find it acceptable for side effects to happen once again, but the return value ignored. And when dropping the "side effects run once again" part, you arrive at an 'init' hook, which I would be in favor of, since it would provide semantics that are sound with regard to user expectations. "Cached get" is just init with extra confusion. Best regards Tim Düsterhus

Tim Düsterhus

1 year ago
Hi Am 2025-07-08 17:10, schrieb Larry Garfield:
> The only way to make the readonliness fully guaranteed would be to > force a readonly property to be cached
Or by not allowing a `get` hook on readonly properties, of course. Best regards Tim Düsterhus

Nick

1 year ago
Hey Tim,
> On 10. Jul 2025, at 17:37, Tim Düsterhus <tim@bastelstu.be> wrote: > > Hi > > Am 2025-07-08 17:10, schrieb Larry Garfield: >> The only way to make the readonliness fully guaranteed would be to force a readonly property to be cached > > Or by not allowing a `get` hook on readonly properties, of course. > > Best regards > Tim Düsterhus
Personally, I would really like to have `get` hooks on readonly properties. Please consider something like this: ```php readonly class Foo { public function __construct( public Output $style, public string $some { get => Output::One === $this->style ? ucfirst($this->some) : strtoupper($this->some); set => '' !== $value ? $value : throw new \Exception(); } ) {} } ``` Easy-to-digest one-liners. Concerns remain separated. Set takes care of validation, get formats. If `get` would not be allowed, we couldn’t do such an obvious thing. For what reason? Instead we would need to delegate formatting to the `set` hook which is messy. ```php readonly class Foo { public function __construct( public Output $style, public string $some { set => '' !== $value ? (Output::One === $this->style ? ucfirst($value) : strtoupper($value)) : throw new \Exception(); } ) {} } ``` Now that I have proposed alternative implementations with caching, I don’t see why `get` should not be allowed. Cheers, Nick Aside: I added two links to alternative implementations to the PR description.

Tim Düsterhus

1 year ago
Hi Apologies for the belated reply. I was busy with getting my own implementation wrapped up and the thread was so active that I had troubles keeping up. On 7/11/25 06:20, Nick wrote:
> Personally, I would really like to have `get` hooks on readonly properties. Please consider something like this: > > ```php > readonly class Foo > { > public function __construct( > public Output $style, > public string $some { > get => Output::One === $this->style ? ucfirst($this->some) : strtoupper($this->some); > set => '' !== $value ? $value : throw new \Exception(); > } > ) {} > } > ``` > > Easy-to-digest one-liners. Concerns remain separated. Set takes care of validation, get formats.
I respectfully disagree on the "easy-to-digest" part. A 98 character line containing logic is not easy to digest.
> If `get` would not be allowed, we couldn’t do such an obvious thing. For what reason?
In *this specific instance* the `get` hook would not violate my expectations, but this is not true in general.
> Instead we would need to delegate formatting to the `set` hook which is messy.
Running formatting for every access is messy. And it's messy to needlessly use hooks for something that can just be constructor logic. Since `$some` is *always* assigned when running the constructor, this can just be: readonly class Foo { public string $some; public function __construct( public Output $style, string $some, ) { if ($some === '') { throw new \Exception(); } $this->some = match ($style) { Output::One => ucfirst($some), default => strtoupper($some), }; } } Making `Foo` a plain old data class without any behavior after construction and with very obvious control flow within the constructor. This results in both more efficient and easier to reason about code. Best regards Tim Düsterhus

Rob Landers

1 year ago
On Fri, Jul 18, 2025, at 14:10, Tim Düsterhus wrote:
> Hi > > Apologies for the belated reply. I was busy with getting my own > implementation wrapped up and the thread was so active that I had > troubles keeping up.
Hi Tim, Thanks for taking the time to reply. That said, I would like to address a concern, not about the content of your message, but the timing. On multiple RFCs, you've joined in once the discussions has wound down and a vote is immeninent. At this point, many participants have assumed the key issues are raised and addressed; or at least, reached the point of constructive impasse. Reopening settled threads so close to vote tends to disrupt the process. It forces others to revisit old arguments under time pressure, giving the late comments disproportionate visibility, and risks stalling momentum. I understand that threads move quickly and schedules vary, but if a concern is important enough to raise, it really helps to do so while the discussions are actively evolving. Otherwise, it becomes difficult to engage meaningfully. At a certain point, late feedback stops being helpful and starts to erode the trust and rhythm of the process. — Rob

Niels Dossche

1 year ago
Op zaterdag 19 juli 2025 schreef Rob Landers <rob@bottled.codes>:
> On Fri, Jul 18, 2025, at 14:10, Tim Düsterhus wrote: > > Hi > Apologies for the belated reply. I was busy with getting my own > implementation wrapped up and the thread was so active that I had > troubles keeping up. > > Hi Tim, > Thanks for taking the time to reply. That said, I would like to address a
concern, not about the content of your message, but the timing.
> On multiple RFCs, you've joined in once the discussions has wound down
and a vote is immeninent. At this point, many participants have assumed the key issues are raised and addressed; or at least, reached the point of constructive impasse. Hey To be honest, I find your email a bit strange, perhaps even misdirected. As someone who followed this discussion more quietly, it is absolutely not my impression that the discussion wounded down already.
> Reopening settled threads so close to vote tends to disrupt the process.
It forces others to revisit old arguments under time pressure, giving the late comments disproportionate visibility, and risks stalling momentum. It wasn't closed, so there isn't anything to reopen. Also may I remind you that the call for an impeding vote is the thing that triggers time pressure, not Tim's reply. The goal of having an RFC discussion should be to get consensus during the discussion phase.
> I understand that threads move quickly and schedules vary, but if a
concern is important enough to raise, it really helps to do so while the discussions are actively evolving. Otherwise, it becomes difficult to engage meaningfully. He did raise it. Keeping track of the entire ML discussions is hard, and also difficult time-wise. Tim is one of the people who tries to participate to basically every RFC, doing his part in making sure we end up with the best possible outcome for a feature. I'd call that meaningful. I'd also rather delay a feature than having something sooner that we didn't stand behind completely.
> At a certain point, late feedback stops being helpful and starts to erode
the trust and rhythm of the process. I wouldn't call it late. Rushing this RFC to vote to get it into 8.5, despite there being no clear consensus, is the thing that erodes trust and breaks the rythm of the process.
> — Rob
Kind regards Niels

Nicolas Grekas

1 year ago
Hi Tim, Le mar. 1 juil. 2025 à 16:29, Tim Düsterhus <tim@bastelstu.be> a écrit :
> Hi > > Am 2025-06-09 17:11, schrieb Larry Garfield: > > I also fleshed out the __get mention with an example that shows what > > you can already do today, and in fact could since 8.1 when readonly was > > introduced. The hard guarantee of idempotency has never actually been > > there. (This also speaks to Claude's concern.) > > I don't think this really resolves Claude's concern. While it is > certainly true that the guarantees do not currently hold, I don't > believe this is strong enough of a reason not to provide for stronger > guarantees in a *newly introduced feature*. The point of property hooks > is for me that “dynamic properties” are easier to reason about compared > to `__get()`. As a user when accessing a proper `readonly` property, I > do not want to check if there is a property hook that might result in > non-readonly behavior. As an engine developer I want to be able to > optimize based on the `readonly`-ness of a property. Without such > guarantees, the “readonly” keyword does not provide value to me. > > I also believe the LazyProduct example to be broken, since lazy-loading > individual properties might result in an object that is internally > consistent if the database changes in-between.
Here are two situations that are perfectly valid use cases for the example: - event-sourced / versioned entities in the DB, where the state of an object cannot change in the backend - lazy-parsed network payloads, where one parses only part of some JSONs on demand (mongodb and symfony/json-streamer do such things already) Nicolas

Claude Pache

1 year ago
> Le 8 juin 2025 à 06:16, Larry Garfield <larry@garfieldtech.com> a écrit : > > As Nick has graciously provided an implementation, we would like to open discussion on this very small RFC to allow `readonly` on backed properties even if they have a hook defined. > > https://wiki.php.net/rfc/readonly_hooks > > -- > Larry Garfield > larry@garfieldtech.com
Hi Larry, Nick, Last summer, the question of allowing hooks on readonly has been raised as part of the RFC «Property hooks improvements», and at that time I have raised an objection on allowing the get hook on readonly properties and I have suggested for a better design for the main issue it was supposed to solve, see https://externals.io/message/124149#124187 and the following messages. (The RFC itself was trimmed down to the non-controversial part.) I’ll repeat here both my objection and my proposal for better design, but more strongly, with the hope that the message will be received. The purpose of readonly properties is (citing the original RFC, https://wiki.php.net/rfc/readonly_properties_v2#rationale) to provide strong immutable guarantee, i.e.: ```php class Test { public readonly string $prop; public function method(Closure $fn) { $prop = $this->prop; $fn(); // Any code may run here. $prop2 = $this->prop; assert($prop === $prop2); // Always holds. } } ``` By allowing a get hook on readonly property, you are effectively nullifying this invariant. Invariants must be enforced be the engines (whenever possible; there is an inevitable loophole until the property is initialised), and not left to the discretion of the user. If a get hook on readonly property is allowed, a random user will use its creativity in order to circumvent the intended invariant (recall: immutability). I say “creativity”, not “dumbness”, because you cannot mechanically tell the two apart: ```php class doc { public readonly int page { get => $this->page + $this->offset; } private int $offset = 0; public function __construct(int $page) { $this->page = $page; } public function foo() { // $this->offset may be adjusted here } } ``` I know that some people won’t see a problem with that code (see the cited thread above), and this is a strong reason not to allow that: you cannot trust the user to enforce invariants that they don’t understand or are not interested in. (The objection above is for the `get` hook`; there is no such issue with the `set` hook.) Now, here is the suggestion for a better alternative design, that (1) don’t allow to break the invariant of immutability, (2) solve the issue of lazy initialisation (which is, I guess, the main purpose of the `get` hook on readonly), and (3) also works with nullable properties: Add an additional hook to backed properties, named `init`. When attempting to read the value of the backing store, if it is uninitialised, then the init hook is triggered, which is supposed to initialise it. —Claude

Larry Garfield

1 year ago
On Sat, Jun 7, 2025, at 11:16 PM, Larry Garfield wrote:
> As Nick has graciously provided an implementation, we would like to > open discussion on this very small RFC to allow `readonly` on backed > properties even if they have a hook defined. > > https://wiki.php.net/rfc/readonly_hooks
After some back and forth on the PR to settle on error messages, this RFC seems ready. Baring any other feedback we will open the vote on it sometime on Wednesday. --Larry Garfield

Nick

1 year ago
Hey all,
> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: > > As Nick has graciously provided an implementation, we would like to open discussion on this very small RFC to allow `readonly` on backed properties even if they have a hook defined. > > https://wiki.php.net/rfc/readonly_hooks > > -- > Larry Garfield > larry@garfieldtech.com
To not get this buried in individual answers to others: I came up with two alternative implementations which cache the computed `get` hook value. One leverages separate cache properties, the other writes directly to the backing store. Links to the alternative branches can be found in the description of the original PR. https://github.com/php/php-src/pull/18757 I believe that these are fair solutions to address the concerns that came up in the discussion, and I hope people will agree. Cheers, Nick

Claude Pache

1 year ago
> Le 11 juil. 2025 à 06:30, Nick <php@nicksdot.dev> a écrit : > > Hey all, > >> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: >> >> As Nick has graciously provided an implementation, we would like to open discussion on this very small RFC to allow `readonly` on backed properties even if they have a hook defined. >> >> https://wiki.php.net/rfc/readonly_hooks >> >> -- >> Larry Garfield >> larry@garfieldtech.com > > To not get this buried in individual answers to others: > > I came up with two alternative implementations which cache the computed `get` hook value. > One leverages separate cache properties, the other writes directly to the backing store. > > Links to the alternative branches can be found in the description of the original PR. > https://github.com/php/php-src/pull/18757 > > I believe that these are fair solutions to address the concerns that came up in the discussion, and I hope people will agree. > > Cheers, > Nick
Hi Nick, I think that the second alternative described as: “Cache computed get hook to it's backing store; never run the hook again” is near the most reasonable from my point of view (basing my judgment on the description, as I have not looked the actual implementation), although there are still some concerns. Advantages of that approach: 1. relatively to the first alternative solution: there is indeed no point to have a cache separate from the backing-store, as the backing-store is supposed to play that role in most cases; 2. relatively to the manual “??=” pattern, it works correctly with nullable properties. Here are my remaining concerns: A. It may be confusing to have a getter that is not always called. B. The idea of returning the value directly from the backing-store if initialised is useful also for non-readonly properties. (That can be emulated with the “??=” pattern, but only if the property is not nullable.) Both concerns may be resolved with the following amendment: * Introduce a `cached` modifier, that enables the caching semantics (i.e., not executing the getter if the backing-store is initialised). The example from the RFC would be written as: ```php readonly class LazyProduct extends Product { private DbConnection $dbApi; private string $categoryId; public Category $category { cached get => $this->dbApi->loadCategory($this->categoryId); } } ``` The `cached` modifier may be applied to any get hook, but it is mandatory if the property is readonly. (In practice, the “cached get hook” corresponds to my originally proposed “init hook”, but with the advantage of not having a separate hook.) —Claude

Nick

1 year ago
> On 11. Jul 2025, at 14:25, Claude Pache <claude.pache@gmail.com> wrote: > >> Le 11 juil. 2025 à 06:30, Nick <php@nicksdot.dev> a écrit : >> >> To not get this buried in individual answers to others: >> >> I came up with two alternative implementations which cache the computed `get` hook value. >> One leverages separate cache properties, the other writes directly to the backing store. >> >> Links to the alternative branches can be found in the description of the original PR. >> https://github.com/php/php-src/pull/18757 >> >> I believe that these are fair solutions to address the concerns that came up in the discussion, and I hope people will agree. >> >> Cheers, >> Nick > > > Hi Nick, > > I think that the second alternative described as: > > “Cache computed get hook to it's backing store; never run the hook again” > > is near the most reasonable from my point of view (basing my judgment on the description, as I have not looked the actual implementation), although there are still some concerns. > > Advantages of that approach: > > 1. relatively to the first alternative solution: there is indeed no point to have a cache separate from the backing-store, as the backing-store is supposed to play that role in most cases; > > 2. relatively to the manual “??=” pattern, it works correctly with nullable properties. > > Here are my remaining concerns: > > A. It may be confusing to have a getter that is not always called. > > B. The idea of returning the value directly from the backing-store if initialised is useful also for non-readonly properties. (That can be emulated with the “??=” pattern, but only if the property is not nullable.) > > Both concerns may be resolved with the following amendment: > > * Introduce a `cached` modifier, that enables the caching semantics (i.e., not executing the getter if the backing-store is initialised). > > The example from the RFC would be written as: > > ```php > readonly class LazyProduct extends Product { > > private DbConnection $dbApi; > > private string $categoryId; > > public Category $category { > cached get => $this->dbApi->loadCategory($this->categoryId); > } > } > ``` > > The `cached` modifier may be applied to any get hook, but it is mandatory if the property is readonly. > > (In practice, the “cached get hook” corresponds to my originally proposed “init hook”, but with the advantage of not having a separate hook.) > > —Claude
Hey Claude, I agree, the second alternative is more neat.
> A.
Three people seem to think that always running the hook when returning a cached value would cause confusion. You are now arguing for the exact opposite. I don’t have a very strong opinion here. However, I also came to the conclusion that caching and not running the hook is the more obvious and less confusing approach. Also, naturally it’s more performant to not run the hook again. Running the hook again, but then not updating the value feels off. And updating the value would mean no caching, which brings us to the very beginning and your very concern. So, yeah… that’s that. :)
> B.
I am afraid here I do have a strong opinion. Please remember my very first mail before discussion [1]. While for Larry the main reason for `readonly` hooks is lazy-initialisation, for me it is to write less code, to have less noisy classes. I don’t want to be forced to add readonly to each property. Now you are proposing a mandatory `cached` modifier. Which means *checks notes*, I would save 2 characters on each property. You will understand that this is not in my interest, and not what I am proposing here. But aside from that I do not like to write more code. I honestly don’t see the benefit of having this modifier in the first place. How “alternative implementation 2” works now is IMO just good. And it makes sense because it is limited to the `readonly` context. There is no technical need for the modifier. As we see, because we have a working solution at hand. For me: It is clear, it’s solving the main issue you rightfully brought up, and it is less code to write. I can imagine that `cached` could be helpful on non `readonly` properties in some cases. However, I feel that brings us almost in similar territory than immutable classes. It is a whole new topic which I believe really shouldn’t be part of this RFC that explicitly . Can we please agree on that it is future scope whether or not non `readonly` hooks should get such a modifier? I’d appreciate if we could settle with “alternative implementation 2” as proposed. Cheers, Nick [1] https://externals.io/message/127529

Claude Pache

1 year ago
> Le 11 juil. 2025 à 11:38, Nick <php@nicksdot.dev> a écrit : > > > I am afraid here I do have a strong opinion. Please remember my very first mail before discussion [1]. > While for Larry the main reason for `readonly` hooks is lazy-initialisation, for me it is to write less code, to have less noisy classes. > I don’t want to be forced to add readonly to each property. > Now you are proposing a mandatory `cached` modifier. Which means *checks notes*, I would save 2 characters on each property. > You will understand that this is not in my interest, and not what I am proposing here. > > But aside from that I do not like to write more code. I honestly don’t see the benefit of having this modifier in the first place. > How “alternative implementation 2” works now is IMO just good. And it makes sense because it is limited to the `readonly` context. > There is no technical need for the modifier. As we see, because we have a working solution at hand. >
The reason I prefer an explicit `cached` modifier (as opposed to have it implied by the fact that the property is readonly), is because it changes the semantics in the following ways: * the get hook is bypassed in more situations than non-cached get hooks; * (depending on the exact implementation details), the result of the get hook is used to populate the backing-store. (Alternatively, we *could* leave the get hook populate it, and just check that the value it returns matches the value on the backing-store. The latter check is mandatory, but I think we could automatically initialise the backing-store if needed.) By contrast, modifiers like `private` or `readonly` do not change the semantics, but only add restrictions on when the relevant operation is allowed. But this is just my opinion for making code more obvious (as opposed to save few keystrokes). If other people agree that `cached` may be implied by `readonly`, I won’t fight against that.
> Can we please agree on that it is future scope whether or not non `readonly` hooks should get such a modifier?
Personally, I don’t have objection to relegate non-readonly cached get hooks to future scope. —Claude

Marc

1 year ago
On 11.07.25 11:38, Nick wrote:
> >> On 11. Jul 2025, at 14:25, Claude Pache <claude.pache@gmail.com> wrote: >> >>> Le 11 juil. 2025 à 06:30, Nick <php@nicksdot.dev> a écrit : >>> >>> To not get this buried in individual answers to others: >>> >>> I came up with two alternative implementations which cache the >>> computed `get` hook value. >>> One leverages separate cache properties, the other writes directly >>> to the backing store. >>> >>> Links to the alternative branches can be found in the description of >>> the original PR. >>> https://github.com/php/php-src/pull/18757 >>> >>> I believe that these are fair solutions to address the concerns that >>> came up in the discussion, and I hope people will agree. >>> >>> *Cheers,* >>> Nick >> >> >> Hi Nick, >> >> I think that the second alternative described as: >> >> “Cache computed get hook to it's backing store; never run the hook again” >> >> is near the most reasonable from my point of view (basing my judgment >> on the description, as I have not looked the actual implementation), >> although there are still some concerns. >> >> Advantages of that approach: >> >> 1. relatively to the first alternative solution: there is indeed no >> point to have a cache separate from the backing-store, as the >> backing-store is supposed to play that role in most cases; >> >> 2. relatively to the manual “??=” pattern, it works correctly with >> nullable properties. >> >> Here are my remaining concerns: >> >> A. It may be confusing to have a getter that is not always called. >> >> B. The idea of returning the value directly from the backing-store if >> initialised is useful also for non-readonly properties. (That can be >> emulated with the “??=” pattern, but only if the property is not >> nullable.) >> >> Both concerns may be resolved with the following amendment: >> >> * Introduce a `cached` modifier, that enables the caching semantics >> (i.e., not executing the getter if the backing-store is initialised). >> >> The example from the RFC would be written as: >> >> ```php >> readonly class LazyProduct extends Product { >> >>    private DbConnection $dbApi; >>    private string $categoryId; >>    public Category $category { >>        cached get => $this->dbApi->loadCategory($this->categoryId); >>    } >> } >> ``` >> >> The `cached` modifier may be applied to any get hook, but it is >> mandatory if the property is readonly. >> >> (In practice, the “cached get hook” corresponds to my originally >> proposed “init hook”, but with the advantage of not having a separate >> hook.) >> >> —Claude > > Hey Claude, > > I agree, the second alternative is more neat. > >> A. > > Three people seem to think that always running the hook when returning > a cached value would cause confusion. > You are now arguing for the exact opposite. I don’t have a very strong > opinion here. > However, I also came to the conclusion that caching and not running > the hook is the more obvious and less confusing approach. > Also, naturally it’s more performant to not run the hook again. > > Running the hook again, but then not updating the value feels off. > And updating the value would mean no caching, which brings us to the > very beginning and your very concern. > So, yeah… that’s that. :) > >> B. > > I am afraid here I do have a strong opinion. Please remember my very > first mail before discussion [1]. > While for Larry the main reason for `readonly` hooks is > lazy-initialisation, for me it is to write less code, to have less > noisy classes. > I don’t want to be forced to add readonly to each property. > Now you are proposing a mandatory `cached` modifier. Which means > *checks notes*, I would save 2 characters on each property. > You will understand that this is not in my interest, and not what I am > proposing here. > > But aside from that I do not like to write more code. I honestly don’t > see the benefit of having this modifier in the first place. > How “alternative implementation 2” works now is IMO just good. And it > makes sense because it is limited to the `readonly` context. > There is no technical need for the modifier. As we see, because we > have a working solution at hand. > > For me: It is clear, it’s solving the main issue you rightfully > brought up, and it is less code to write. > > I can imagine that `cached` could be helpful on non `readonly` > properties in some cases. > However, I feel that brings us almost in similar territory than > immutable classes. > It is a whole new topic which I believe really shouldn’t be part of > this RFC that explicitly . > Can we please agree on that it is future scope whether or not non > `readonly` hooks should get such a modifier? > > I’d appreciate if we could settle with “alternative implementation 2” > as proposed.
Hi Nick, Claude, I think it's important to explicitly mark it as "this value will be stored in memory", I mean just silently caching the get hook could quickly lead to unexpected behavior. Like one would expect the value to be changed and another one wonders why a big chunk of memory will not be freed `get => $this->readBigFile();`. On the one hand I like the cached modifier but personally I would prefer a separate init hook because it seems to be more clear that this is a backed property that will be initialized once. The cached modifier I would expect to be an attribute applicable to any function which uses another cache store similar to how it's possible in python to memorize function calls which would be a very different feature. Just my two cents from someone without voting rights

Nick

1 year ago
> On 13. Jul 2025, at 20:38, Marc Bennewitz <marc@mabe.berlin> wrote: > Hi Nick, Claude,
Hey Marc,
> I think it's important to explicitly mark it as "this value will be stored in memory", I mean just silently caching the get hook could quickly lead to unexpected behavior. Like one would expect the value to be changed >
The most here made the argument that "a changing value from a readonly get hook" would be the unexpected behaviour. That’s why we ended up with the current “alternative implementation 2”. Please see my last answer to Rob for a fair example [1] . The current preferred alternative implementation covers both situations... If a property is `readonly`: - you can set once - on read you always get the same (once computed) value back If a property is NOT `readonly`: - you can set often - on read you always get the fresh (often computed) value back I argue that this is a very easy mental model. I hope that voters agree on “cached may be implied by readonly”, as Claude called it.
> and another one wonders why a big chunk of memory will not be freed `get => $this->readBigFile();`. >
Where do you see non-freed memory in one but not the other? - in both scenarios, readonly or not, the `readBigFile()` will end up in memory. - on each consecutive property call the usage in both scenarios is the same. - when assigning a call the same property to multiple tmp vars the cached, once-computed version uses less memory than the non-cached version Do I miss something? Did I misunderstand something? Additionally, the cached version has the benefit that the expensive computation only happens once.
> On the one hand I like the cached modifier but personally I would prefer a separate init hook because it seems to be more clear that this is a backed property that will be initialized once. >
To have an `init` hook doesn’t solve the get hook issue.
> The cached modifier I would expect to be an attribute applicable to any function which uses another cache store similar to how it's possible in python to memorize function calls which would be a very different feature. >
As earlier answered to Claude [2], I seek to write less code. To introduce a `cached` modifier voids this for no strong reason (please see “mental model” above).
-- Cheers, Nick [1] https://news-web.php.net/php.internals/128010 [2] https://news-web.php.net/php.internals/128007

Marc

1 year ago
On 13.07.25 18:17, Nick wrote:
> >> On 13. Jul 2025, at 20:38, Marc Bennewitz <marc@mabe.berlin> wrote: >> >> Hi Nick, Claude, >> > Hey Marc, >> >> I think it's important to explicitly mark it as "this value will be >> stored in memory", I mean just silently caching the get hook could >> quickly lead to unexpected behavior. Like one would expect the value >> to be changed >> > The most here made the argument that "a changing value from a readonly > get hook" would be the unexpected behaviour. > That’s why we ended up with the current “alternative implementation > 2”. Please see my last answer to Rob for a fair example [1] . > > The current preferred alternative implementation covers both situations... > > If a property is `readonly`: > - you can set *once* > - on read you always get the same (_once_ computed) value back
This is exactly the behavior I mean which is somehow unexpected if not marked explicitly as the result could be cached and the property value will never change. Not being able to write to something doesn't generally mean reading the value will never change. `get => random_int(0, 100);` I don't want to say that the path we want to go with readonly being able to assume the value will never change is wrong but I think it's not clear for the user and can be missed quickly - even with a test as you have to read the property multiple time to notice the difference.
> > If a property is NOT `readonly`: > - you can set *often* > - on read you always get the fresh (_often_ computed) value back > > I argue that this is a very easy mental model. > I hope that voters agree on “|cached| may be implied by |readonly|”, > as Claude called it.
All what I'm saying is that this behavior should be explicit and not applied implicitly on a readonly get hook.
> >> and another one wonders why a big chunk of memory will not be freed >> `get => $this->readBigFile();`. >> > Where do you see non-freed memory in one but not the other? > > - in both scenarios, readonly or not, the `readBigFile()` will end up > in memory. > - on each consecutive property call the usage in both scenarios is the > same. > - when assigning a call the same property to multiple tmp vars the > cached, once-computed version uses less memory than the non-cached version > > Do I miss something? Did I misunderstand something?
yes, in both cases the data will end up in memory until all references are garbaged. The difference is that the object of that property now has a reference as well and needs to be destroyed as well be able to free the memory.
> > Additionally, the cached version has the benefit that the expensive > computation only happens once. >> >> On the one hand I like the cached modifier but personally I would >> prefer a separate init hook because it seems to be more clear that >> this is a backed property that will be initialized once. >> > To have an `init` hook doesn’t solve the get hook issue.
As far as I understood the init hook it would still disallow readonly+get but allow readonly+init and init would be called once the first time the property gets read and the result would end up in the backing store. As a result you get the same behavior as `cached get` with the only difference that you write `init => random_int();` instead of `cached get => random_int();` Additionally it can be used to initialize a property for non readonly properties as well. ``` class Test {     public int $seek {         init => random_int(0, 100);   // called once on read if not initialized         get => $this->seek + 100;         set => $this->seek = $value + 100;     } } ``` var_dump((new Test())->seek); // number between 100-200 OR 200-300 depending of the set hook be called once with the result of init as well. Or did I misunderstand it?
>> The cached modifier I would expect to be an attribute applicable to >> any function which uses another cache store similar to how it's >> possible in python to memorize function calls which would be a very >> different feature. >> > As earlier answered to Claude [2], I seek to write less code. To > introduce a `cached` modifier voids this for no strong reason (please > see “mental model” above).
See above - and `init` is just once more character.

Eric Norris

1 year ago
On Sun, Jul 13, 2025 at 2:00 PM Marc Bennewitz <marc@mabe.berlin> wrote:
> > > On 13.07.25 18:17, Nick wrote: > > > On 13. Jul 2025, at 20:38, Marc Bennewitz <marc@mabe.berlin> wrote: > > Hi Nick, Claude, > > Hey Marc, > > I think it's important to explicitly mark it as "this value will be stored in memory", I mean just silently caching the get hook could quickly lead to unexpected behavior. Like one would expect the value to be changed > > The most here made the argument that "a changing value from a readonly get hook" would be the unexpected behaviour. > That’s why we ended up with the current “alternative implementation 2”. Please see my last answer to Rob for a fair example [1] . > > The current preferred alternative implementation covers both situations... > > If a property is `readonly`: > - you can set once > - on read you always get the same (once computed) value back > > This is exactly the behavior I mean which is somehow unexpected if not marked explicitly as the result could be cached and the property value will never change. > > Not being able to write to something doesn't generally mean reading the value will never change. `get => random_int(0, 100);` > > I don't want to say that the path we want to go with readonly being able to assume the value will never change is wrong but I think it's not clear for the user and can be missed quickly - even with a test as you have to read the property multiple time to notice the difference. > > > If a property is NOT `readonly`: > - you can set often > - on read you always get the fresh (often computed) value back > > I argue that this is a very easy mental model. > I hope that voters agree on “cached may be implied by readonly”, as Claude called it. > > All what I'm saying is that this behavior should be explicit and not applied implicitly on a readonly get hook.
I agree with Marc here, not surprisingly.
> To have an `init` hook doesn’t solve the get hook issue. > > As far as I understood the init hook it would still disallow readonly+get but allow readonly+init and init would be called once the first time the property gets read and the result would end up in the backing store. > > As a result you get the same behavior as `cached get` with the only difference that you write `init => random_int();` instead of `cached get => random_int();`
Agreed. Nick, I am not sure how the init hook doesn't "solve the get hook issue". As I understand it, the get hook issue is that get hooks are not allowed on readonly properties. The init hook would be identical to a "cached get" hook on a readonly property, so why doesn't it solve the issue?
> Or did I misunderstand it?
I share the same understanding as you, Marc.
> The cached modifier I would expect to be an attribute applicable to any function which uses another cache store similar to how it's possible in python to memorize function calls which would be a very different feature. > > As earlier answered to Claude [2], I seek to write less code. To introduce a `cached` modifier voids this for no strong reason (please see “mental model” above). > > See above - and `init` is just once more character.
I was going to respond to this point earlier, but Marc beat me to it. An "init" hook is one more character than a get hook, is explicit over a get hook that works differently only for readonly properties, *and* is far fewer characters than the explicit "cached" modifier get hook option. On top of that, as Claude mentioned an init hook provides the ability to differentiate between a null property and an uninitialized property - an init hook would only be called for uninitialized properties, so no need for $this->foo ??= "bar".

Nick

1 year ago
> On 14. Jul 2025, at 01:15, Eric Norris <eric.t.norris@gmail.com> wrote: > > On Sun, Jul 13, 2025 at 2:00 PM Marc Bennewitz <marc@mabe.berlin> wrote: >> >> >> On 13.07.25 18:17, Nick wrote: >> >> >> On 13. Jul 2025, at 20:38, Marc Bennewitz <marc@mabe.berlin> wrote: >> >> Hi Nick, Claude, >> >> Hey Marc, >> >> I think it's important to explicitly mark it as "this value will be stored in memory", I mean just silently caching the get hook could quickly lead to unexpected behavior. Like one would expect the value to be changed >> >> The most here made the argument that "a changing value from a readonly get hook" would be the unexpected behaviour. >> That’s why we ended up with the current “alternative implementation 2”. Please see my last answer to Rob for a fair example [1] . >> >> The current preferred alternative implementation covers both situations... >> >> If a property is `readonly`: >> - you can set once >> - on read you always get the same (once computed) value back >> >> This is exactly the behavior I mean which is somehow unexpected if not marked explicitly as the result could be cached and the property value will never change. >> >> Not being able to write to something doesn't generally mean reading the value will never change. `get => random_int(0, 100);` >> >> I don't want to say that the path we want to go with readonly being able to assume the value will never change is wrong but I think it's not clear for the user and can be missed quickly - even with a test as you have to read the property multiple time to notice the difference. >> >> >> If a property is NOT `readonly`: >> - you can set often >> - on read you always get the fresh (often computed) value back >> >> I argue that this is a very easy mental model. >> I hope that voters agree on “cached may be implied by readonly”, as Claude called it. >> >> All what I'm saying is that this behavior should be explicit and not applied implicitly on a readonly get hook. > > I agree with Marc here, not surprisingly. > >> To have an `init` hook doesn’t solve the get hook issue. >> >> As far as I understood the init hook it would still disallow readonly+get but allow readonly+init and init would be called once the first time the property gets read and the result would end up in the backing store. >> >> As a result you get the same behavior as `cached get` with the only difference that you write `init => random_int();` instead of `cached get => random_int();` > > Agreed. Nick, I am not sure how the init hook doesn't "solve the get > hook issue". As I understand it, the get hook issue is that get hooks > are not allowed on readonly properties. The init hook would be > identical to a "cached get" hook on a readonly property, so why > doesn't it solve the issue? > >> Or did I misunderstand it? > > I share the same understanding as you, Marc. > >> The cached modifier I would expect to be an attribute applicable to any function which uses another cache store similar to how it's possible in python to memorize function calls which would be a very different feature. >> >> As earlier answered to Claude [2], I seek to write less code. To introduce a `cached` modifier voids this for no strong reason (please see “mental model” above). >> >> See above - and `init` is just once more character. > > I was going to respond to this point earlier, but Marc beat me to it. > An "init" hook is one more character than a get hook, is explicit over > a get hook that works differently only for readonly properties, *and* > is far fewer characters than the explicit "cached" modifier get hook > option. > > On top of that, as Claude mentioned an init hook provides the ability > to differentiate between a null property and an uninitialized property > - an init hook would only be called for uninitialized properties, so > no need for $this->foo ??= "bar”.
Hey Marc, Hey Eric, A) Init hook Marc,
> Or did I misunderstand it?
Well, I don’t know. Everyone seems to think of init hooks (and their playing together with other hooks) differently. Some say this, some say that. That’s the exact issue. Want an example? Eric just agreed with your code example which has a get hook AND init hook.
>> ``` >> class Test { >> public int $seek { >> init => random_int(0, 100); // called once on read if not initialized >> get => $this->seek + 100; >> set => $this->seek = $value + 100; >> } >> } >> ``` >> var_dump((new Test())->seek); // number between 100-200 OR 200-300 depending of the set hook be called once with the result of init as well. >> Or did I misunderstand it? > > I share the same understanding as you, Marc.
But one mail before he answered to Larry:
> I think, at least for readonly, you couldn't have an init hook and a > get hook, since the main objection here is to having get hooks on > readonly properties at all. On normal properties, I think that'd be > okay?
So what is it? Get hook cool, or not? And how does an init hook work exactly? How play all combinations together? And how with readonly? Why didn’t your example use readonly if we talk about readonly hooks? I don’t know all that. And that’s why I have proposed what I proposed. We have a reasonable solution for set/get, without init, right in front of us; and millions of devs could benefit from it the next release. Eric, why it wouldn’t be solved by an init hook? Well, because as you said, readonly get hooks would not be allowed in combination with init. Others apparently have different opinions. So will they, will they not? I, however, want get/set on readonly properties. And that is what I proposed here. An init hook is not part of this proposal and I am not planning to take this on. This RFC, however, would not block anyone from creating their own RFC for init hooks. B) Less Code Marc was talking about init, cache modifier and attributes in the same time. Claude initially wanted a cache modifier on each hook. I argue that “readonly implicates cached” is very reasonable here. Many, many opinions. Many, many options. All have their pros, and cons. All can be attacked, and defended. :) All I want is the below (less code; and no dealing with unrelated things just because I want to add hooks to a readonly class). ```php // I have a nice readonly class final readonly class Entry { public function __construct( public string $word, public string $slug, ) {} } // I simply want to add a hooked-property to that readonly class final readonly class Entry { public function __construct( public string $word, public string $slug, public array $terms { set(array $value) => array_map(static function (Term|array $term): Term { return $term instanceof Term ? $term : new Term(...$term); }, $value); get => $this->terms; // something, something }, ) {} } // but I cannot. I need to: // - make the class non-readonly, // - add some readonly here and there, // - deal with async visibility // to eventually end up with this final class Entry // cannot be readonly, annoying { public function __construct( public readonly string $word, // annoying readonly public readonly string $slug, // annoying readonly private(set) array $terms { // requires visibility set(array $value) => array_map(static function (Term|array $term): Term { return $term instanceof Term ? $term : new Term(...$term); }, $value); get => $this->terms; }, ) {} } ``` Adding hook to a readonly class really should not be THAT hard and demanding. And for that, I believe, I provided a solution that is easy to reason about (all details in previous mails), and allows everyone to achieve what they want. Cheers, Nick

Eric Norris

1 year ago
> Well, I don’t know. Everyone seems to think of init hooks (and their playing together with other hooks) differently. > Some say this, some say that. That’s the exact issue. Want an example? > > Eric just agreed with your code example which has a get hook AND init hook. > > ``` > class Test { > public int $seek { > init => random_int(0, 100); // called once on read if not initialized > get => $this->seek + 100; > set => $this->seek = $value + 100; > } > } > ``` > > var_dump((new Test())->seek); // number between 100-200 OR 200-300 depending of the set hook be called once with the result of init as well. > > Or did I misunderstand it? > > > I share the same understanding as you, Marc. > > > But one mail before he answered to Larry: > > I think, at least for readonly, you couldn't have an init hook and a > get hook, since the main objection here is to having get hooks on > readonly properties at all. On normal properties, I think that'd be > okay? > > > So what is it? Get hook cool, or not? And how does an init hook work exactly? > How play all combinations together? And how with readonly? > Why didn’t your example use readonly if we talk about readonly hooks?
I believe you are attempting to point out a contradiction, but as far as I can tell my two statements are consistent - his example did not include readonly, and it makes sense to me as-is. My prior statement to Larry is "I think, at least for readonly, you couldn't have an init hook and a get hook". Again, his example is not readonly, so it's consistent.
> Eric, > why it wouldn’t be solved by an init hook? Well, because as you said, readonly get hooks would not be allowed in combination with init. > Others apparently have different opinions. So will they, will they not?
I don't think others have different opinions, or at least I haven't heard someone say that readonly get hooks should be allowed with init.
> > I, however, want get/set on readonly properties. And that is what I proposed here. > > An init hook is not part of this proposal and I am not planning to take this on. > This RFC, however, would not block anyone from creating their own RFC for init hooks. > > B) Less Code > > Marc was talking about init, cache modifier and attributes in the same time. > Claude initially wanted a cache modifier on each hook. > > I argue that “readonly implicates cached” is very reasonable here. > > Many, many opinions. Many, many options. All have their pros, and cons. All can be attacked, and defended. :) > > All I want is the below (less code; and no dealing with unrelated things just because I want to add hooks to a readonly class). > > ```php > > // I have a nice readonly class > final readonly class Entry > > { > public function __construct( > public string $word, > public string $slug, > ) {} > } > > // I simply want to add a hooked-property to that readonly class > final readonly class Entry > > { > public function __construct( > public string $word, > public string $slug, > public array $terms { > set(array $value) => array_map(static function (Term|array $term): Term { > return $term instanceof Term ? $term : new Term(...$term); > }, $value); > get => $this->terms; // something, something > }, > ) {} > } >
Your example might not need a "get" hook at all, if I understand correctly? The default behavior would make sense here (and, it seems for at least some part of the mailing list, is the *only* thing that makes sense). You would need a "set" hook, but I don't think anyone is objecting to that. If you'd like to propose "set" hooks in isolation, it seems like it would pass.
> And for that, I believe, I provided a solution that is easy to reason about (all details in previous mails), and allows everyone to achieve what they want.
I appreciate that you are focused on solving a real-world problem, and that you have implemented a solution that addresses the problem. I can understand that it seems like it's in reach, and that this conversation might feel like we're wasting time. That said, I think a number of people have reservations with the semantics of get hooks with readonly, and it's important that we take the time to ensure this feature is one that makes sense to all developers, and continues to push the language in the direction of being more consistent and hard to misuse. In an earlier email, Larry said, "`readonly` has always been misnamed; it should really be `writeonce`, because that's all it is. (Once again, this is likely the most poorly designed feature we've added in many years.)". I hope that I'm not misconstruing what he meant, but this makes it seem especially prudent to avoid making additional mistakes with regard to readonly.

Ilija Tovilo

1 year ago
Hi Nick On Fri, Jul 11, 2025 at 6:31 AM Nick <php@nicksdot.dev> wrote:
> >> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: >> >> https://wiki.php.net/rfc/readonly_hooks >> >> To not get this buried in individual answers to others: > > I came up with two alternative implementations which cache the computed `get` hook value. > One leverages separate cache properties, the other writes directly to the backing store. > > Links to the alternative branches can be found in the description of the original PR. > https://github.com/php/php-src/pull/18757
I am not a fan of the caching approach. The implementation draft for this approach [^1] works by storing the assigned value in the property slot, and replacing it with the value returned from get one called for the first time. One of the issues here is that the backing value is observable without calling get. For example: ``` class C { public public(set) readonly string $prop { get => strtoupper($this->prop); } } $c = new C(); $c->prop = 'foo'; var_dump(((array)$c)['prop']); // foo $c->prop; var_dump(((array)$c)['prop']); // FOO ``` Here we can see that the underlying value changes, despite the readonly declaration. This is especially problematic for things like [un]serialize(), where calling serialize() before or after accessing the property will change which underlying value is serialized. Even worse, we don't actually know whether an unserialized property has already called the get hook. ``` class C { public public(set) readonly int $prop { get => $this->prop + 1; } } $c = new C(); $c->prop = 1; $s1 = serialize($c); $c->prop; $s2 = serialize($c); var_dump(unserialize($s1)->prop); // int(2) var_dump(unserialize($s2)->prop); // int(3) ``` Currently, get is always called after unserialize(). There may be similar issues for __clone(). For readable and writable properties, the straight-forward solution is to move the logic to set. ``` class C { public public(set) readonly int $prop { set => $value + 1; } } ``` This is slightly differently, semantically, in that it executes any potential side-effects on write rather than read, which seems reasonable. This also avoids the implicit mutation mentioned previously. At least in these cases, disallowing readonly + get seems reasonable to me. I will say that this doesn't solve all get+set cases. For example, proxies. Hopefully, lazy objects can mostly bridge this gap. Another case is lazy getters. ``` class C { public readonly int $magicNumber { get => expensiveComputation(); } } ``` This does not seem to work in the current implementation:
> Fatal error: Hooked virtual properties cannot be declared readonly
I presume it would be possible to fix this, e.g. by using readonly as a marker to add a backing value to the property. I'm personally not too fond of making the rules on which properties are backed more complicated, as this is already a common cause for confusion. I also fundamentally don't like that readonly changes whether get is called. Currently, if hooks are present, they are called. This adds more special cases to an already complex feature. To me it seems the primary motivator for this RFC are readonly classes, i.e. to prevent the addition of hooks from breaking readonly classes. However, as lazy-getters are de-facto read-only, given they are only writable from the extremely narrow scope of the hook itself, the modifier doesn't do much. Maybe an easier solution would be to provide an opt-out of readonly. Side note: Your implementation has a bug: ``` class C { public public(set) readonly int $prop { get => $this->prop + 1; } } function test($c) { var_dump($c->prop); } $c = new C(); $c->prop = 1; test($c); // int(2) test($c); // int(3) ``` You likely need to dodge the fast path in the VM by not marking the property as "SIMPLE_GET" [^2]. Sorry if this e-mail is a bit all over the place, I had trouble structuring it in a more sensible way. Ilija [^1]: https://github.com/php/php-src/compare/master...NickSdot:php-php-src:readonly-hooks-once [^2]: https://github.com/php/php-src/blob/4d9fc506df1131c630c530a0bfa6d0338cffa03c/Zend/zend_object_handlers.c#L864

Larry Garfield

1 year ago
On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote:
> Hi Nick > > On Fri, Jul 11, 2025 at 6:31 AM Nick <php@nicksdot.dev> wrote: >> >>> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: >>> >>> https://wiki.php.net/rfc/readonly_hooks >>> >>> To not get this buried in individual answers to others: >> >> I came up with two alternative implementations which cache the computed `get` hook value. >> One leverages separate cache properties, the other writes directly to the backing store. >> >> Links to the alternative branches can be found in the description of the original PR. >> https://github.com/php/php-src/pull/18757 > > I am not a fan of the caching approach. The implementation draft for > this approach [^1] works by storing the assigned value in the property > slot, and replacing it with the value returned from get one called for > the first time. One of the issues here is that the backing value is > observable without calling get. For example: > > ``` > class C { > public public(set) readonly string $prop { > get => strtoupper($this->prop); > } > } > $c = new C(); > $c->prop = 'foo'; > var_dump(((array)$c)['prop']); // foo > $c->prop; > var_dump(((array)$c)['prop']); // FOO > ``` > > Here we can see that the underlying value changes, despite the > readonly declaration. This is especially problematic for things like > [un]serialize(), where calling serialize() before or after accessing > the property will change which underlying value is serialized. Even > worse, we don't actually know whether an unserialized property has > already called the get hook. > > ``` > class C { > public public(set) readonly int $prop { > get => $this->prop + 1; > } > } > $c = new C(); > $c->prop = 1; > $s1 = serialize($c); > $c->prop; > $s2 = serialize($c); > var_dump(unserialize($s1)->prop); // int(2) > var_dump(unserialize($s2)->prop); // int(3) > ``` > > Currently, get is always called after unserialize(). There may be > similar issues for __clone(). > > For readable and writable properties, the straight-forward solution is > to move the logic to set. > > ``` > class C { > public public(set) readonly int $prop { > set => $value + 1; > } > } > ``` > > This is slightly differently, semantically, in that it executes any > potential side-effects on write rather than read, which seems > reasonable. This also avoids the implicit mutation mentioned > previously. At least in these cases, disallowing readonly + get seems > reasonable to me. I will say that this doesn't solve all get+set > cases. For example, proxies. Hopefully, lazy objects can mostly bridge > this gap. > > Another case is lazy getters. > > ``` > class C { > public readonly int $magicNumber { > get => expensiveComputation(); > } > } > ``` > > This does not seem to work in the current implementation: > >> Fatal error: Hooked virtual properties cannot be declared readonly > > I presume it would be possible to fix this, e.g. by using readonly as > a marker to add a backing value to the property. I'm personally not > too fond of making the rules on which properties are backed more > complicated, as this is already a common cause for confusion. I also > fundamentally don't like that readonly changes whether get is called. > Currently, if hooks are present, they are called. This adds more > special cases to an already complex feature. > > To me it seems the primary motivator for this RFC are readonly > classes, i.e. to prevent the addition of hooks from breaking readonly > classes. However, as lazy-getters are de-facto read-only, given they > are only writable from the extremely narrow scope of the hook itself, > the modifier doesn't do much. Maybe an easier solution would be to > provide an opt-out of readonly.
Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. That would allow conditional set hooks, conditional gets, caching gets (like we already have with ??=), and so on. The mental model is simple and easy to explain/document. The behavior is the same as with methods. But the identity of the stored value would be consistent. It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. Would that way of looking at it be acceptable to folks? --Larry Garfield

Nicolas Grekas

1 year ago
Le lun. 14 juil. 2025 à 15:41, Larry Garfield <larry@garfieldtech.com> a écrit :
> On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote: > > Hi Nick > > > > On Fri, Jul 11, 2025 at 6:31 AM Nick <php@nicksdot.dev> wrote: > >> > >>> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> > wrote: > >>> > >>> https://wiki.php.net/rfc/readonly_hooks > >>> > >>> To not get this buried in individual answers to others: > >> > >> I came up with two alternative implementations which cache the computed > `get` hook value. > >> One leverages separate cache properties, the other writes directly to > the backing store. > >> > >> Links to the alternative branches can be found in the description of > the original PR. > >> https://github.com/php/php-src/pull/18757 > > > > I am not a fan of the caching approach. The implementation draft for > > this approach [^1] works by storing the assigned value in the property > > slot, and replacing it with the value returned from get one called for > > the first time. One of the issues here is that the backing value is > > observable without calling get. For example: > > > > ``` > > class C { > > public public(set) readonly string $prop { > > get => strtoupper($this->prop); > > } > > } > > $c = new C(); > > $c->prop = 'foo'; > > var_dump(((array)$c)['prop']); // foo > > $c->prop; > > var_dump(((array)$c)['prop']); // FOO > > ``` > > > > Here we can see that the underlying value changes, despite the > > readonly declaration. This is especially problematic for things like > > [un]serialize(), where calling serialize() before or after accessing > > the property will change which underlying value is serialized. Even > > worse, we don't actually know whether an unserialized property has > > already called the get hook. > > > > ``` > > class C { > > public public(set) readonly int $prop { > > get => $this->prop + 1; > > } > > } > > $c = new C(); > > $c->prop = 1; > > $s1 = serialize($c); > > $c->prop; > > $s2 = serialize($c); > > var_dump(unserialize($s1)->prop); // int(2) > > var_dump(unserialize($s2)->prop); // int(3) > > ``` > > > > Currently, get is always called after unserialize(). There may be > > similar issues for __clone(). > > > > For readable and writable properties, the straight-forward solution is > > to move the logic to set. > > > > ``` > > class C { > > public public(set) readonly int $prop { > > set => $value + 1; > > } > > } > > ``` > > > > This is slightly differently, semantically, in that it executes any > > potential side-effects on write rather than read, which seems > > reasonable. This also avoids the implicit mutation mentioned > > previously. At least in these cases, disallowing readonly + get seems > > reasonable to me. I will say that this doesn't solve all get+set > > cases. For example, proxies. Hopefully, lazy objects can mostly bridge > > this gap. > > > > Another case is lazy getters. > > > > ``` > > class C { > > public readonly int $magicNumber { > > get => expensiveComputation(); > > } > > } > > ``` > > > > This does not seem to work in the current implementation: > > > >> Fatal error: Hooked virtual properties cannot be declared readonly > > > > I presume it would be possible to fix this, e.g. by using readonly as > > a marker to add a backing value to the property. I'm personally not > > too fond of making the rules on which properties are backed more > > complicated, as this is already a common cause for confusion. I also > > fundamentally don't like that readonly changes whether get is called. > > Currently, if hooks are present, they are called. This adds more > > special cases to an already complex feature. > > > > To me it seems the primary motivator for this RFC are readonly > > classes, i.e. to prevent the addition of hooks from breaking readonly > > classes. However, as lazy-getters are de-facto read-only, given they > > are only writable from the extremely narrow scope of the hook itself, > > the modifier doesn't do much. Maybe an easier solution would be to > > provide an opt-out of readonly. > > Thanks, Ilija. You expressed my concerns as well. And yes, in practice, > readonly classes over-reaching is the main use case; if you're marking > individual properties readonly, then just don't mark the one that has a > hook on it (use aviz if needed) and there's no issue. > > Perhaps we're thinking about this the wrong way, though? So far we've > talked as though readonly makes the property write-once. But... what if we > think of it as applying to the field, aka the backing value? > > So readonly doesn't limit calling the get hook, or even the set hook, > multiple times. Only writing to the actual value in the object table. > That gives the exact same set of guarantees that a getX()/setX() method > would give. The methods can be called any number of times, but the stored > value can only be written once. > > That would allow conditional set hooks, conditional gets, caching gets > (like we already have with ??=), and so on. The mental model is simple and > easy to explain/document. The behavior is the same as with methods. But > the identity of the stored value would be consistent. > > It would not guarantee $foo->bar === $foo->bar in all cases (though that > would likely hold in the 99% case in practice), but then, $foo->getBar() > === $foo->getBar() has never been guaranteed either. > > Would that way of looking at it be acceptable to folks? >
It does to me: readonly applies to the backed property, then hooks add behavior as see fit. This is especially useful to intercept accesses to said properties. Without readonly hooks, designing an abstract API that uses readonly properties is a risky decision since it blocks any (future) implementation that needs this interception capability. As a forward-thinking author, one currently has two choices: not using readonly properties in abstract APIs, or falling back to using getter/setters. That's a design failure for hooks IMHO. I'm glad this RFC exists to fill this gap. Nicolas

Rob Landers

1 year ago
On Tue, Jul 15, 2025, at 19:27, Nicolas Grekas wrote:
> > > Le lun. 14 juil. 2025 à 15:41, Larry Garfield <larry@garfieldtech.com> a écrit : >> On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote: >> > Hi Nick >> > >> > On Fri, Jul 11, 2025 at 6:31 AM Nick <php@nicksdot.dev> wrote: >> >> >> >>> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: >> >>> >> >>> https://wiki.php.net/rfc/readonly_hooks >> >>> >> >>> To not get this buried in individual answers to others: >> >> >> >> I came up with two alternative implementations which cache the computed `get` hook value. >> >> One leverages separate cache properties, the other writes directly to the backing store. >> >> >> >> Links to the alternative branches can be found in the description of the original PR. >> >> https://github.com/php/php-src/pull/18757 >> > >> > I am not a fan of the caching approach. The implementation draft for >> > this approach [^1] works by storing the assigned value in the property >> > slot, and replacing it with the value returned from get one called for >> > the first time. One of the issues here is that the backing value is >> > observable without calling get. For example: >> > >> > ``` >> > class C { >> > public public(set) readonly string $prop { >> > get => strtoupper($this->prop); >> > } >> > } >> > $c = new C(); >> > $c->prop = 'foo'; >> > var_dump(((array)$c)['prop']); // foo >> > $c->prop; >> > var_dump(((array)$c)['prop']); // FOO >> > ``` >> > >> > Here we can see that the underlying value changes, despite the >> > readonly declaration. This is especially problematic for things like >> > [un]serialize(), where calling serialize() before or after accessing >> > the property will change which underlying value is serialized. Even >> > worse, we don't actually know whether an unserialized property has >> > already called the get hook. >> > >> > ``` >> > class C { >> > public public(set) readonly int $prop { >> > get => $this->prop + 1; >> > } >> > } >> > $c = new C(); >> > $c->prop = 1; >> > $s1 = serialize($c); >> > $c->prop; >> > $s2 = serialize($c); >> > var_dump(unserialize($s1)->prop); // int(2) >> > var_dump(unserialize($s2)->prop); // int(3) >> > ``` >> > >> > Currently, get is always called after unserialize(). There may be >> > similar issues for __clone(). >> > >> > For readable and writable properties, the straight-forward solution is >> > to move the logic to set. >> > >> > ``` >> > class C { >> > public public(set) readonly int $prop { >> > set => $value + 1; >> > } >> > } >> > ``` >> > >> > This is slightly differently, semantically, in that it executes any >> > potential side-effects on write rather than read, which seems >> > reasonable. This also avoids the implicit mutation mentioned >> > previously. At least in these cases, disallowing readonly + get seems >> > reasonable to me. I will say that this doesn't solve all get+set >> > cases. For example, proxies. Hopefully, lazy objects can mostly bridge >> > this gap. >> > >> > Another case is lazy getters. >> > >> > ``` >> > class C { >> > public readonly int $magicNumber { >> > get => expensiveComputation(); >> > } >> > } >> > ``` >> > >> > This does not seem to work in the current implementation: >> > >> >> Fatal error: Hooked virtual properties cannot be declared readonly >> > >> > I presume it would be possible to fix this, e.g. by using readonly as >> > a marker to add a backing value to the property. I'm personally not >> > too fond of making the rules on which properties are backed more >> > complicated, as this is already a common cause for confusion. I also >> > fundamentally don't like that readonly changes whether get is called. >> > Currently, if hooks are present, they are called. This adds more >> > special cases to an already complex feature. >> > >> > To me it seems the primary motivator for this RFC are readonly >> > classes, i.e. to prevent the addition of hooks from breaking readonly >> > classes. However, as lazy-getters are de-facto read-only, given they >> > are only writable from the extremely narrow scope of the hook itself, >> > the modifier doesn't do much. Maybe an easier solution would be to >> > provide an opt-out of readonly. >> >> Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. >> >> Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? >> >> So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. >> >> That would allow conditional set hooks, conditional gets, caching gets (like we already have with ??=), and so on. The mental model is simple and easy to explain/document. The behavior is the same as with methods. But the identity of the stored value would be consistent. >> >> It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. >> >> Would that way of looking at it be acceptable to folks? > > It does to me: readonly applies to the backed property, then hooks add behavior as see fit. This is especially useful to intercept accesses to said properties. Without readonly hooks, designing an abstract API that uses readonly properties is a risky decision since it blocks any (future) implementation that needs this interception capability. As a forward-thinking author, one currently has two choices: not using readonly properties in abstract APIs, or falling back to using getter/setters. That's a design failure for hooks IMHO. I'm glad this RFC exists to fill this gap. > > Nicolas
To add to this, as I just mentioned on the Records thread, it would be good to get hooks on readonly objects. With the new clone(), there is no way to rely on validation in constructors. The most robust validation in 8.5 can only be done via set/get hooks, but these hooks are not available on readonly classes. This means that it is remarkably easy to "break" objects that do constructor validation + use public(set) -- or use clone() in inherited objects instead of the parent constructor. In my experience, readonly objects typically only do constructor validation (DRY). — Rob

Eric Norris

1 year ago
On Thu, Jul 17, 2025 at 3:31 AM Rob Landers <rob@bottled.codes> wrote:
> > On Tue, Jul 15, 2025, at 19:27, Nicolas Grekas wrote: > > > > Le lun. 14 juil. 2025 à 15:41, Larry Garfield <larry@garfieldtech.com> a écrit : > > On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote: > > Hi Nick > > > > On Fri, Jul 11, 2025 at 6:31 AM Nick <php@nicksdot.dev> wrote: > >> > >>> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: > >>> > >>> https://wiki.php.net/rfc/readonly_hooks > >>> > >>> To not get this buried in individual answers to others: > >> > >> I came up with two alternative implementations which cache the computed `get` hook value. > >> One leverages separate cache properties, the other writes directly to the backing store. > >> > >> Links to the alternative branches can be found in the description of the original PR. > >> https://github.com/php/php-src/pull/18757 > > > > I am not a fan of the caching approach. The implementation draft for > > this approach [^1] works by storing the assigned value in the property > > slot, and replacing it with the value returned from get one called for > > the first time. One of the issues here is that the backing value is > > observable without calling get. For example: > > > > ``` > > class C { > > public public(set) readonly string $prop { > > get => strtoupper($this->prop); > > } > > } > > $c = new C(); > > $c->prop = 'foo'; > > var_dump(((array)$c)['prop']); // foo > > $c->prop; > > var_dump(((array)$c)['prop']); // FOO > > ``` > > > > Here we can see that the underlying value changes, despite the > > readonly declaration. This is especially problematic for things like > > [un]serialize(), where calling serialize() before or after accessing > > the property will change which underlying value is serialized. Even > > worse, we don't actually know whether an unserialized property has > > already called the get hook. > > > > ``` > > class C { > > public public(set) readonly int $prop { > > get => $this->prop + 1; > > } > > } > > $c = new C(); > > $c->prop = 1; > > $s1 = serialize($c); > > $c->prop; > > $s2 = serialize($c); > > var_dump(unserialize($s1)->prop); // int(2) > > var_dump(unserialize($s2)->prop); // int(3) > > ``` > > > > Currently, get is always called after unserialize(). There may be > > similar issues for __clone(). > > > > For readable and writable properties, the straight-forward solution is > > to move the logic to set. > > > > ``` > > class C { > > public public(set) readonly int $prop { > > set => $value + 1; > > } > > } > > ``` > > > > This is slightly differently, semantically, in that it executes any > > potential side-effects on write rather than read, which seems > > reasonable. This also avoids the implicit mutation mentioned > > previously. At least in these cases, disallowing readonly + get seems > > reasonable to me. I will say that this doesn't solve all get+set > > cases. For example, proxies. Hopefully, lazy objects can mostly bridge > > this gap. > > > > Another case is lazy getters. > > > > ``` > > class C { > > public readonly int $magicNumber { > > get => expensiveComputation(); > > } > > } > > ``` > > > > This does not seem to work in the current implementation: > > > >> Fatal error: Hooked virtual properties cannot be declared readonly > > > > I presume it would be possible to fix this, e.g. by using readonly as > > a marker to add a backing value to the property. I'm personally not > > too fond of making the rules on which properties are backed more > > complicated, as this is already a common cause for confusion. I also > > fundamentally don't like that readonly changes whether get is called. > > Currently, if hooks are present, they are called. This adds more > > special cases to an already complex feature. > > > > To me it seems the primary motivator for this RFC are readonly > > classes, i.e. to prevent the addition of hooks from breaking readonly > > classes. However, as lazy-getters are de-facto read-only, given they > > are only writable from the extremely narrow scope of the hook itself, > > the modifier doesn't do much. Maybe an easier solution would be to > > provide an opt-out of readonly. > > Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. > > Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? > > So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. > > That would allow conditional set hooks, conditional gets, caching gets (like we already have with ??=), and so on. The mental model is simple and easy to explain/document. The behavior is the same as with methods. But the identity of the stored value would be consistent. > > It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. > > Would that way of looking at it be acceptable to folks? > > > It does to me: readonly applies to the backed property, then hooks add behavior as see fit. This is especially useful to intercept accesses to said properties. Without readonly hooks, designing an abstract API that uses readonly properties is a risky decision since it blocks any (future) implementation that needs this interception capability. As a forward-thinking author, one currently has two choices: not using readonly properties in abstract APIs, or falling back to using getter/setters. That's a design failure for hooks IMHO. I'm glad this RFC exists to fill this gap. > > Nicolas > > > To add to this, as I just mentioned on the Records thread, it would be good to get hooks on readonly objects. With the new clone(), there is no way to rely on validation in constructors. The most robust validation in 8.5 can only be done via set/get hooks, but these hooks are not available on readonly classes. This means that it is remarkably easy to "break" objects that do constructor validation + use public(set) -- or use clone() in inherited objects instead of the parent constructor. In my experience, readonly objects typically only do constructor validation (DRY).
(shoot, double post, sorry Rob) I'm not sure I follow - do you actually need both `set` and `get` hooks for validation? I would think only `set` hooks would be necessary, and I don't yet think I've seen an objection to `set` hooks for `readonly`.

Rob Landers

1 year ago
On Thu, Jul 17, 2025, at 15:10, Eric Norris wrote:
> On Thu, Jul 17, 2025 at 3:31 AM Rob Landers <rob@bottled.codes> wrote: > > > > On Tue, Jul 15, 2025, at 19:27, Nicolas Grekas wrote: > > > > > > > > Le lun. 14 juil. 2025 à 15:41, Larry Garfield <larry@garfieldtech.com> a écrit : > > > > On Sun, Jul 13, 2025, at 6:28 PM, Ilija Tovilo wrote: > > > Hi Nick > > > > > > On Fri, Jul 11, 2025 at 6:31 AM Nick <php@nicksdot.dev> wrote: > > >> > > >>> On 8. Jun 2025, at 11:16, Larry Garfield <larry@garfieldtech.com> wrote: > > >>> > > >>> https://wiki.php.net/rfc/readonly_hooks > > >>> > > >>> To not get this buried in individual answers to others: > > >> > > >> I came up with two alternative implementations which cache the computed `get` hook value. > > >> One leverages separate cache properties, the other writes directly to the backing store. > > >> > > >> Links to the alternative branches can be found in the description of the original PR. > > >> https://github.com/php/php-src/pull/18757 > > > > > > I am not a fan of the caching approach. The implementation draft for > > > this approach [^1] works by storing the assigned value in the property > > > slot, and replacing it with the value returned from get one called for > > > the first time. One of the issues here is that the backing value is > > > observable without calling get. For example: > > > > > > ``` > > > class C { > > > public public(set) readonly string $prop { > > > get => strtoupper($this->prop); > > > } > > > } > > > $c = new C(); > > > $c->prop = 'foo'; > > > var_dump(((array)$c)['prop']); // foo > > > $c->prop; > > > var_dump(((array)$c)['prop']); // FOO > > > ``` > > > > > > Here we can see that the underlying value changes, despite the > > > readonly declaration. This is especially problematic for things like > > > [un]serialize(), where calling serialize() before or after accessing > > > the property will change which underlying value is serialized. Even > > > worse, we don't actually know whether an unserialized property has > > > already called the get hook. > > > > > > ``` > > > class C { > > > public public(set) readonly int $prop { > > > get => $this->prop + 1; > > > } > > > } > > > $c = new C(); > > > $c->prop = 1; > > > $s1 = serialize($c); > > > $c->prop; > > > $s2 = serialize($c); > > > var_dump(unserialize($s1)->prop); // int(2) > > > var_dump(unserialize($s2)->prop); // int(3) > > > ``` > > > > > > Currently, get is always called after unserialize(). There may be > > > similar issues for __clone(). > > > > > > For readable and writable properties, the straight-forward solution is > > > to move the logic to set. > > > > > > ``` > > > class C { > > > public public(set) readonly int $prop { > > > set => $value + 1; > > > } > > > } > > > ``` > > > > > > This is slightly differently, semantically, in that it executes any > > > potential side-effects on write rather than read, which seems > > > reasonable. This also avoids the implicit mutation mentioned > > > previously. At least in these cases, disallowing readonly + get seems > > > reasonable to me. I will say that this doesn't solve all get+set > > > cases. For example, proxies. Hopefully, lazy objects can mostly bridge > > > this gap. > > > > > > Another case is lazy getters. > > > > > > ``` > > > class C { > > > public readonly int $magicNumber { > > > get => expensiveComputation(); > > > } > > > } > > > ``` > > > > > > This does not seem to work in the current implementation: > > > > > >> Fatal error: Hooked virtual properties cannot be declared readonly > > > > > > I presume it would be possible to fix this, e.g. by using readonly as > > > a marker to add a backing value to the property. I'm personally not > > > too fond of making the rules on which properties are backed more > > > complicated, as this is already a common cause for confusion. I also > > > fundamentally don't like that readonly changes whether get is called. > > > Currently, if hooks are present, they are called. This adds more > > > special cases to an already complex feature. > > > > > > To me it seems the primary motivator for this RFC are readonly > > > classes, i.e. to prevent the addition of hooks from breaking readonly > > > classes. However, as lazy-getters are de-facto read-only, given they > > > are only writable from the extremely narrow scope of the hook itself, > > > the modifier doesn't do much. Maybe an easier solution would be to > > > provide an opt-out of readonly. > > > > Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. > > > > Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? > > > > So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. > > > > That would allow conditional set hooks, conditional gets, caching gets (like we already have with ??=), and so on. The mental model is simple and easy to explain/document. The behavior is the same as with methods. But the identity of the stored value would be consistent. > > > > It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. > > > > Would that way of looking at it be acceptable to folks? > > > > > > It does to me: readonly applies to the backed property, then hooks add behavior as see fit. This is especially useful to intercept accesses to said properties. Without readonly hooks, designing an abstract API that uses readonly properties is a risky decision since it blocks any (future) implementation that needs this interception capability. As a forward-thinking author, one currently has two choices: not using readonly properties in abstract APIs, or falling back to using getter/setters. That's a design failure for hooks IMHO. I'm glad this RFC exists to fill this gap. > > > > Nicolas > > > > > > To add to this, as I just mentioned on the Records thread, it would be good to get hooks on readonly objects. With the new clone(), there is no way to rely on validation in constructors. The most robust validation in 8.5 can only be done via set/get hooks, but these hooks are not available on readonly classes. This means that it is remarkably easy to "break" objects that do constructor validation + use public(set) -- or use clone() in inherited objects instead of the parent constructor. In my experience, readonly objects typically only do constructor validation (DRY). > > (shoot, double post, sorry Rob) > > I'm not sure I follow - do you actually need both `set` and `get` > hooks for validation? I would think only `set` hooks would be > necessary, and I don't yet think I've seen an objection to `set` hooks > for `readonly`. >
It depends... for example, you might have an isValid property which computes whether or not the object is valid, or in the case of lazy properties, detecting an invalid state there. You also might put validation in getters because you're building up the object over several lines, thus it might only be in a partially valid state during construction: readonly class User { public public(set) $first_name; public public(set) $last_name; public $name { get => implode(' ', [$this->first_name, $this->last_name]) } } $user->first_name = "Rob" echo $user->name; // oops Or something. In this case, we can rely on the "uninitialized property" exception to be raised if it isn't yet fully valid, but you might want to throw your own exception. — Rob

Tim Düsterhus

1 year ago
Hi On 7/14/25 15:38, Larry Garfield wrote:
> Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue.
A readonly class is not just a convenience shortcut to mark each individual property as readonly. It has important semantics of its own, because it forces child classes to also be readonly. And even for final classes it communicates to the user that "I won't be adding non-readonly properties to the class". Marking a class as readonly must therefore be a deliberate decision, since it affects the public API of your class and in turn also user expectations.
> Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value?
I think of readonly from the view of the public API surface of an object. The property hooks RFC was very explicit in that property hooks are intended to be “transparent to the user” and can be added without breaking the public API. In other words: Whether or not a property is implemented using a hook should be considered an implementation detail and as a user of a class I do not care whether there is a backing value or not.
> So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once.
As a user of a class the "backing table" is mostly inaccessible to me when interacting with objects. It's only exposed via var_dump() and serialize(), the former of which is a debug functionality and the output of latter not something I must touch.
> It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either.
Properties and methods are something different. For methods there a reasonable expectation that *behavior* is associated with them, for properties there is not. A 99% case is not sufficient for me to rely on when there's explicit communication by the class author that I may rely on properties not suddenly changing. Best regards Tim Düsterhus

Rob Landers

1 year ago
On Fri, Jul 18, 2025, at 17:25, Tim Düsterhus wrote:
> Hi > > On 7/14/25 15:38, Larry Garfield wrote: > > Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. > > A readonly class is not just a convenience shortcut to mark each > individual property as readonly. It has important semantics of its own, > because it forces child classes to also be readonly. And even for final > classes it communicates to the user that "I won't be adding non-readonly > properties to the class".
Wasn’t that the entire point of readonly classes? Because it was painful to write readonly for every property. Then if a property is readonly, the inherited property is also readonly, so, by extension: a class extending a readonly class is also readonly. There’s no “communication” here; just logic.
> > Marking a class as readonly must therefore be a deliberate decision, > since it affects the public API of your class and in turn also user > expectations.
Not really. I can remove the readonly designation and manually mark every property as readonly. The behavior of the class doesn’t magically change. Or, at least, I hope it doesn’t.
> > > Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? > > I think of readonly from the view of the public API surface of an > object. The property hooks RFC was very explicit in that property hooks > are intended to be “transparent to the user” and can be added without > breaking the public API. In other words: Whether or not a property is > implemented using a hook should be considered an implementation detail > and as a user of a class I do not care whether there is a backing value > or not. > > > So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. > > As a user of a class the "backing table" is mostly inaccessible to me > when interacting with objects. It's only exposed via var_dump() and > serialize(), the former of which is a debug functionality and the output > of latter not something I must touch. > > > It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. > > Properties and methods are something different. For methods there a > reasonable expectation that *behavior* is associated with them, for > properties there is not.
Unless I missed something. Hooks are fancy methods? There is nothing intrinsic about object properties. There is nothing that says two calls to the same property’s getters are going to result in the same values. There is asynchronous php, declare ticks, etc. especially in the case of globals, there is no guarantee you even have the same object. At the end of the day, it is up to the programmer building that system / program to provide those guarantees— not the language.
> > A 99% case is not sufficient for me to rely on when there's explicit > communication by the class author that I may rely on properties not > suddenly changing. > > Best regards > Tim Düsterhus >
— Rob

Eric Norris

1 year ago
On Fri, Jul 18, 2025 at 12:01 PM Rob Landers <rob@bottled.codes> wrote:
> > > > On Fri, Jul 18, 2025, at 17:25, Tim Düsterhus wrote: > > Hi > > On 7/14/25 15:38, Larry Garfield wrote: > > Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. > > A readonly class is not just a convenience shortcut to mark each > individual property as readonly. It has important semantics of its own, > because it forces child classes to also be readonly. And even for final > classes it communicates to the user that "I won't be adding non-readonly > properties to the class". > > > Wasn’t that the entire point of readonly classes? Because it was painful to write readonly for every property. Then if a property is readonly, the inherited property is also readonly, so, by extension: a class extending a readonly class is also readonly. > > There’s no “communication” here; just logic. > > > Marking a class as readonly must therefore be a deliberate decision, > since it affects the public API of your class and in turn also user > expectations. > > > Not really. I can remove the readonly designation and manually mark every property as readonly. The behavior of the class doesn’t magically change. Or, at least, I hope it doesn’t. > > > > Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? > > I think of readonly from the view of the public API surface of an > object. The property hooks RFC was very explicit in that property hooks > are intended to be “transparent to the user” and can be added without > breaking the public API. In other words: Whether or not a property is > implemented using a hook should be considered an implementation detail > and as a user of a class I do not care whether there is a backing value > or not. > > > So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. > > As a user of a class the "backing table" is mostly inaccessible to me > when interacting with objects. It's only exposed via var_dump() and > serialize(), the former of which is a debug functionality and the output > of latter not something I must touch. > > > It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. > > Properties and methods are something different. For methods there a > reasonable expectation that *behavior* is associated with them, for > properties there is not. > > > Unless I missed something. Hooks are fancy methods? There is nothing intrinsic about object properties. There is nothing that says two calls to the same property’s getters are going to result in the same values. There is asynchronous php, declare ticks, etc. especially in the case of globals, there is no guarantee you even have the same object. At the end of the day, it is up to the programmer building that system / program to provide those guarantees— not the language.
I do think that, without any additional information, it would be reasonable to assume that `$foo->bar === $foo->bar`, i.e. there would not be side-effects until you've called a method or written to the object in some way. So I share Tim's opinion here, but I do agree that with hooks available this is not actually a guarantee. You could certainly have a `$foo->random_value` property and document that it will be different each time you call it. That said, once the programmer has added the readonly designation to a property, I do think that something says that two calls to the same property will result in the same values - the readonly designation. I disagree with the point that it's not up to the language - the language should provide an affordance for enforcing programmer intent, and I see no reason to even have a readonly designation if we're going to make it easily circumventable or otherwise just a "hint". It seems that one common counterpoint to the "let's not make it circumventable" argument is to point out that it's already circumventable via __get. I agree with Claude that this is not a justification for making it *easier* to circumvent. I would also like to note that the original RFC (https://wiki.php.net/rfc/readonly_properties_v2#unset) seems to allow this behavior *for the purpose of lazy initialization*. With an `init` hook, we'd have solved this problem, and could deprecate the `__get` hack for `readonly` properties / classes. Nicolas Grekas said "__get is certainly not legacy; removing it would break many use cases without proper alternatives.", but note that I'm only suggesting we could maybe deprecate __get for `readonly` properties once we had an `init` hook - I'm not proposing deprecating it generally. Without a counterexample, I don't think there would be another reason for `__get` to work with `readonly` properties.

Rob Landers

1 year ago
On Fri, Jul 18, 2025, at 18:48, Eric Norris wrote:
> On Fri, Jul 18, 2025 at 12:01 PM Rob Landers <rob@bottled.codes> wrote: > > > > > > > > On Fri, Jul 18, 2025, at 17:25, Tim Düsterhus wrote: > > > > Hi > > > > On 7/14/25 15:38, Larry Garfield wrote: > > > Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. > > > > A readonly class is not just a convenience shortcut to mark each > > individual property as readonly. It has important semantics of its own, > > because it forces child classes to also be readonly. And even for final > > classes it communicates to the user that "I won't be adding non-readonly > > properties to the class". > > > > > > Wasn’t that the entire point of readonly classes? Because it was painful to write readonly for every property. Then if a property is readonly, the inherited property is also readonly, so, by extension: a class extending a readonly class is also readonly. > > > > There’s no “communication” here; just logic. > > > > > > Marking a class as readonly must therefore be a deliberate decision, > > since it affects the public API of your class and in turn also user > > expectations. > > > > > > Not really. I can remove the readonly designation and manually mark every property as readonly. The behavior of the class doesn’t magically change. Or, at least, I hope it doesn’t. > > > > > > > Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? > > > > I think of readonly from the view of the public API surface of an > > object. The property hooks RFC was very explicit in that property hooks > > are intended to be “transparent to the user” and can be added without > > breaking the public API. In other words: Whether or not a property is > > implemented using a hook should be considered an implementation detail > > and as a user of a class I do not care whether there is a backing value > > or not. > > > > > So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. > > > > As a user of a class the "backing table" is mostly inaccessible to me > > when interacting with objects. It's only exposed via var_dump() and > > serialize(), the former of which is a debug functionality and the output > > of latter not something I must touch. > > > > > It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. > > > > Properties and methods are something different. For methods there a > > reasonable expectation that *behavior* is associated with them, for > > properties there is not. > > > > > > Unless I missed something. Hooks are fancy methods? There is nothing intrinsic about object properties. There is nothing that says two calls to the same property’s getters are going to result in the same values. There is asynchronous php, declare ticks, etc. especially in the case of globals, there is no guarantee you even have the same object. At the end of the day, it is up to the programmer building that system / program to provide those guarantees— not the language. > > I do think that, without any additional information, it would be > reasonable to assume that `$foo->bar === $foo->bar`, i.e. there would > not be side-effects until you've called a method or written to the > object in some way. So I share Tim's opinion here, but I do agree that > with hooks available this is not actually a guarantee. You could > certainly have a `$foo->random_value` property and document that it > will be different each time you call it. > > That said, once the programmer has added the readonly designation to a > property, I do think that something says that two calls to the same > property will result in the same values - the readonly designation. I > disagree with the point that it's not up to the language - the > language should provide an affordance for enforcing programmer intent, > and I see no reason to even have a readonly designation if we're going > to make it easily circumventable or otherwise just a "hint". > > It seems that one common counterpoint to the "let's not make it > circumventable" argument is to point out that it's already > circumventable via __get. I agree with Claude that this is not a > justification for making it *easier* to circumvent. I would also like > to note that the original RFC > (https://wiki.php.net/rfc/readonly_properties_v2#unset) seems to allow > this behavior *for the purpose of lazy initialization*. With an `init` > hook, we'd have solved this problem, and could deprecate the `__get` > hack for `readonly` properties / classes. > > Nicolas Grekas said "__get is certainly not legacy; removing it would > break many use cases without proper alternatives.", but note that I'm > only suggesting we could maybe deprecate __get for `readonly` > properties once we had an `init` hook - I'm not proposing deprecating > it generally. Without a counterexample, I don't think there would be > another reason for `__get` to work with `readonly` properties. >
I personally feel that making special restrictions and affordances to readonly classes is a bad language design. It is a “class” and not something special or different like an enum. This is just a class with its properties made readonly. The word says it all — read. Only. Personally, I don’t use readonly much any more. The amount of restrictions and weird behavior just makes it impossible for any real-world use except for narrow cases the original authors of the feature dreamed up. With hooks and asymmetrical viz, it’s nearly an obsolete feature anyway. — Rob

Nick

1 year ago
> On 18. Jul 2025, at 23:48, Eric Norris <eric.t.norris@gmail.com> wrote: > > On Fri, Jul 18, 2025 at 12:01 PM Rob Landers <rob@bottled.codes <mailto:rob@bottled.codes>> wrote: >> >> >> >> On Fri, Jul 18, 2025, at 17:25, Tim Düsterhus wrote: >> >> Hi >> >> On 7/14/25 15:38, Larry Garfield wrote: >>> Thanks, Ilija. You expressed my concerns as well. And yes, in practice, readonly classes over-reaching is the main use case; if you're marking individual properties readonly, then just don't mark the one that has a hook on it (use aviz if needed) and there's no issue. >> >> A readonly class is not just a convenience shortcut to mark each >> individual property as readonly. It has important semantics of its own, >> because it forces child classes to also be readonly. And even for final >> classes it communicates to the user that "I won't be adding non-readonly >> properties to the class". >> >> >> Wasn’t that the entire point of readonly classes? Because it was painful to write readonly for every property. Then if a property is readonly, the inherited property is also readonly, so, by extension: a class extending a readonly class is also readonly. >> >> There’s no “communication” here; just logic. >> >> >> Marking a class as readonly must therefore be a deliberate decision, >> since it affects the public API of your class and in turn also user >> expectations. >> >> >> Not really. I can remove the readonly designation and manually mark every property as readonly. The behavior of the class doesn’t magically change. Or, at least, I hope it doesn’t. >> >> >>> Perhaps we're thinking about this the wrong way, though? So far we've talked as though readonly makes the property write-once. But... what if we think of it as applying to the field, aka the backing value? >> >> I think of readonly from the view of the public API surface of an >> object. The property hooks RFC was very explicit in that property hooks >> are intended to be “transparent to the user” and can be added without >> breaking the public API. In other words: Whether or not a property is >> implemented using a hook should be considered an implementation detail >> and as a user of a class I do not care whether there is a backing value >> or not. >> >>> So readonly doesn't limit calling the get hook, or even the set hook, multiple times. Only writing to the actual value in the object table. That gives the exact same set of guarantees that a getX()/setX() method would give. The methods can be called any number of times, but the stored value can only be written once. >> >> As a user of a class the "backing table" is mostly inaccessible to me >> when interacting with objects. It's only exposed via var_dump() and >> serialize(), the former of which is a debug functionality and the output >> of latter not something I must touch. >> >>> It would not guarantee $foo->bar === $foo->bar in all cases (though that would likely hold in the 99% case in practice), but then, $foo->getBar() === $foo->getBar() has never been guaranteed either. >> >> Properties and methods are something different. For methods there a >> reasonable expectation that *behavior* is associated with them, for >> properties there is not. >> >> >> Unless I missed something. Hooks are fancy methods? There is nothing intrinsic about object properties. There is nothing that says two calls to the same property’s getters are going to result in the same values. There is asynchronous php, declare ticks, etc. especially in the case of globals, there is no guarantee you even have the same object. At the end of the day, it is up to the programmer building that system / program to provide those guarantees— not the language. > > I do think that, without any additional information, it would be > reasonable to assume that `$foo->bar === $foo->bar`, i.e. there would > not be side-effects until you've called a method or written to the > object in some way. So I share Tim's opinion here, but I do agree that > with hooks available this is not actually a guarantee. You could > certainly have a `$foo->random_value` property and document that it > will be different each time you call it. > > That said, once the programmer has added the readonly designation to a > property, I do think that something says that two calls to the same > property will result in the same values - the readonly designation. I > disagree with the point that it's not up to the language - the > language should provide an affordance for enforcing programmer intent, > and I see no reason to even have a readonly designation if we're going > to make it easily circumventable or otherwise just a "hint". > > It seems that one common counterpoint to the "let's not make it > circumventable" argument is to point out that it's already > circumventable via __get. I agree with Claude that this is not a > justification for making it *easier* to circumvent. I would also like > to note that the original RFC > (https://wiki.php.net/rfc/readonly_properties_v2#unset) seems to allow > this behavior *for the purpose of lazy initialization*. With an `init` > hook, we'd have solved this problem, and could deprecate the `__get` > hack for `readonly` properties / classes. > > Nicolas Grekas said "__get is certainly not legacy; removing it would > break many use cases without proper alternatives.", but note that I'm > only suggesting we could maybe deprecate __get for `readonly` > properties once we had an `init` hook - I'm not proposing deprecating > it generally. Without a counterexample, I don't think there would be > another reason for `__get` to work with `readonly` properties.
Hey all, I allow myself to answer in one single mail, instead to all of you individually. Honestly, I didn’t expect that this RFC will be THAT controversial. 😅 However, I get it. There are good arguments on either side. I did hope that the “implicit cache” is a decent middle ground, but that also didn’t work out as I thought. As mentioned earlier, this is my very first RFC. I am at a point where I am a bit overwhelmed. That said, Larry and I heard you and already decided to offer a split vote to enable us to at least land “set only” in 8.5. If we didn’t misunderstood it, then y’all agreed on `set` (only) should be allowed? This would IMHO already be a huge improvement compared to now; and a low hanging fruit. Not exactly what I wanted, but it is what it is. Long story short. We simply don’t have the time to get `init` sorted before feature freeze. I offer to follow up with a “readonly `init` hook” RFC for 8.6 to sort the rest. I’d appreciate if voters could settle on a yes for “set only” for 8.5. Wdyt? Would this help to get closer to closing the discussion? Cheers, Nick

Tim Düsterhus

1 year ago
Hi [dropping most of the folks from the Cc list to reduce noise a little] On 7/18/25 19:08, Nick wrote:
> As mentioned earlier, this is my very first RFC. I am at a point where I am a bit overwhelmed.
With an RFC touching core language semantics, you've certainly opted to touch a hot topic. Even for me, as an experienced contributor to Internals and the PHP language that was quite a few emails to work through, so I totally get being overwhelmed. I think a big contributor to this is that you've choosen a less-than-ideal time to propose the RFC. As you are well-aware, by now feature freeze is in less than 4 weeks. Many of the core contributors, including myself, are busy with wrapping up the implementation of their own RFCs or helping with the review of others. Keeping track of multiple last-minute RFCs at the same time and carefully thinking about the implications and then, should the RFC pass, also about the implementation is really demanding. I'm in the lucky position that I can spend part of my company time contributing to PHP, proposing RFCs and also providing feedback on other RFCs. But I can't make this my full-time job, so I've specifically set aside several hours my free time today to catch up with the thread to be able to provide my feedback on the RFC. The announcement of the upcoming vote has caused me to send out several emails all over the thread that were less refined than would be usual for me, because I felt the need to get out *something* before it's too late.
> That said, Larry and I heard you and already decided to offer a split vote to enable us to at least land “set only” in 8.5. > If we didn’t misunderstood it, then y’all agreed on `set` (only) should be allowed?
I am unable to come up with arguments against supporting a set hook for readonly properties. While I'm not sure I would be in favor (I would need to think about this more), I would not be against. So I might abstain or I might vote in favor after I had time to fully think about it.
> Not exactly what I wanted, but it is what it is.
I totally understand it's discouraging when needing to wait for another year before being able to make use of one's own contributions. But at the same time any change to the language has a big impact on the ecosystem and needs to work not for just one year, but for 10 or more years. Some things can probably never be removed from the language. So to me it's important to err on the side of caution. "No is temporary, yes is forever".
> I offer to follow up with a “readonly `init` hook” RFC for 8.6 to sort the rest.
I'm certainly happy to help out with figuring out all the important details and possible edge cases for an "init hook" RFC for the PHP 8.6 cycle. Note how I specifically left out the "readonly" there, since I don't think there is a need to restrict "init" to just readonly properties.
> I’d appreciate if voters could settle on a yes for “set only” for 8.5. > > Wdyt? Would this help to get closer to closing the discussion?
From my side, removing the get hook part from the RFC would definitely settle the discussion. Best regards Tim Düsterhus

Michael Cordover

1 year ago
On Fri, 2025-07-18 at 11:49 -04:00, Rob Landers <rob@bottled.codes> wrote:
> On Fri, Jul 18, 2025, at 17:25, Tim Düsterhus wrote: >> A readonly class is not just a convenience shortcut to mark each >> individual property as readonly. It has important semantics of its own, >> because it forces child classes to also be readonly. And even for final >> classes it communicates to the user that "I won't be adding non-readonly >> properties to the class". > > Wasn’t that the entire point of readonly classes? Because it was painful to write readonly for every property. Then if a property is readonly, the inherited property is also readonly, so, by extension: a class extending a readonly class is also readonly. > > There’s no “communication” here; just logic.
All code is communication, not just to the computer, but to other humans reading it. This is why the semantics of the terms are important. I agree with Tim's interpretation of those semantics, and hence vastly prefer an init hook to get in these circumstances. Importantly, this RFC can clarify the semantics of these terms for the language as a whole. That specific meaning can be documented. If a proposal does not come from a principled understanding of what `readonly` means - what it indicates to users of the class - I think that is a step backwards. mjec

Tim Düsterhus

1 year ago
Hi On 7/18/25 17:49, Rob Landers wrote:
> Wasn’t that the entire point of readonly classes? Because it was painful to write readonly for every property.
It was certainly *one point*, but not the *entire* point. Quoting from the RFC (https://wiki.php.net/rfc/readonly_classes): "it's still not easy to declare (quasi-)immutable classes" and then further: "it will prevent the creation of dynamic properties" and "a readonly class can only extend a readonly parent" So the point of the RFC is not "adding readonly for every property is verbose", but "I want to be able to define immutable classes", which, as I outlined before, is something different.
> The behavior of the class doesn’t magically change. Or, at least, I hope it doesn’t.
The behavior of the class changes, since dynamic properties will now be legal. It also breaks any child classes, since child classes of non-readonly classes may not be readonly (not even if all properties are already readonly). The behavior doesn't change magically, though, it changes due to the intentional removal of the `readonly` keyword on the class.
> Unless I missed something. Hooks are fancy methods? There is nothing intrinsic about object properties. There is nothing that says two calls to the same property’s getters are going to result in the same values. There is asynchronous php, declare ticks, etc. especially in the case of globals, there is no guarantee you even have the same object. At the end of the day, it is up to the programmer building that system / program to provide those guarantees— not the language.
I agree with both Eric's response to this paragraph. Best regards Tim Düsterhus

Larry Garfield

1 year ago
On Sat, Jun 7, 2025, at 11:16 PM, Larry Garfield wrote:
> As Nick has graciously provided an implementation, we would like to > open discussion on this very small RFC to allow `readonly` on backed > properties even if they have a hook defined. > > https://wiki.php.net/rfc/readonly_hooks
"Very small" never lasts, does it... :-) Given the lack of consensus both here and in off-list discussions on how to handle get hooks, we have done the following: * Split the RFC into two sections, one for get, one for set. * Expanded and refined the examples for both. The implementation is still the original, however. * Split the vote into two: one for allowing readonly get hooks, one for readonly set hooks. We will start the vote sometime this weekend, most likely, unless some major feedback appears before then, and let the chips fall where they may. --Larry Garfield

Tim Düsterhus

1 year ago
Hi On 7/17/25 18:26, Larry Garfield wrote:
> Given the lack of consensus both here and in off-list discussions on how to handle get hooks, we have done the following: > > * Split the RFC into two sections, one for get, one for set. > * Expanded and refined the examples for both. The implementation is still the original, however. > * Split the vote into two: one for allowing readonly get hooks, one for readonly set hooks. > > We will start the vote sometime this weekend, most likely, unless some major feedback appears before then, and let the chips fall where they may.
After working through (most of) the discussion, I've now taken a look at the updated RFC. I have the following remarks: 1.
> It is really “write-once”, which is not the same as immutable (as shown above). But there's no reason that “write-once” need be incompatible with hooks.
This is a strawman argumentation, as I've outlined in my previous emails, calling readonly "write-once" is wrong. It is a reasonable user expectation to always get the identical value when reading a value that may only be set once. By calling it "write-once" you are trying to shift the focus to the write operation, which is totally irrelevant for user expectations when interacting with readonly properties. Especially for expectations of users that just *use* a class rather than writing one. 2.
> The concern would only appear if someone is deliberately doing something non-stable
Or if someone accidentally calls a (deep) function chain that is non-pure. 3.
> Or, for a more real-world and larger example, PHP 8.4 requires this:
This is false. You are perfectly able to write this in PHP 8.4: final readonly class Entry { public readonly $terms; public function __construct( public string $word, public string $slug, array $terms, ) { $this->terms = $this->upcastTerms($terms); } private function upcastTerms(array $terms): array { $upcast = static fn (Term|array $term): Term => $term instanceof Term ? $term : new Term(...$term); return array_map($upcast, $value) } } In no way do you need to use a property hook. 4.
> // New code in 8.5: > > $p = new PositivePoint(3, 4); > $p2 = clone($p, ['x' => -10]);
This is not legal code in PHP 8.5. Clone-with respects visibility and since your asymmetric visibility RFC included the change, you are probably aware that `readonly` implies `protected(set)`. 5.
> but are now necessary to ensure that invariants are enforced.
And therefore with PHP 8.5 hooks are not necessary to enforce invariants, except in the rare case where a `public(set) readonly` property is used. 6.
> So no guarantees are softened by this RFC.
Yes, they are. Unless `__get()` is implemented on a class (which is explicitly visible as part of the public API), readonly guarantees the immutability of identity. 7.
> While that is an interesting idea that has been floated a few times, it has enough complexities and edge cases of its own to address that we feel it is out of scope.
While it certainly is your right as the RFC authors to consider certain things out of scope for an RFC, I strongly oppose the notion of shipping something that is strictly inferior and comes with obvious semantic issues due to perceived complexity of another solution and then following up with the proper solution that has already been identified. As I've outlined in my previous emails, I found defining semantics for an 'init' hook straight-forward when looking at how PHP works as of today. 8.
> However, this RFC is in no way incompatible with adding an init hook in the future should it be proposed.
This is true, but as I've mentioned before, an 'init' hook would enable the same use cases without bringing along issues. So it really should be "one of them, but not both" (with "one of them" being the init hook). -------- After reading through the discussion, it seems the only argument against the 'init' hook is perceived complexity. It is not at all clear to me why this means that we must now rush something with clear issues into PHP 8.5. Best regards Tim Düsterhus

Nicolas Grekas

1 year ago
Le ven. 18 juil. 2025 à 18:32, Tim Düsterhus <tim@bastelstu.be> a écrit :
> Hi > > On 7/17/25 18:26, Larry Garfield wrote: > > Given the lack of consensus both here and in off-list discussions on how > to handle get hooks, we have done the following: > > > > * Split the RFC into two sections, one for get, one for set. > > * Expanded and refined the examples for both. The implementation is > still the original, however. > > * Split the vote into two: one for allowing readonly get hooks, one for > readonly set hooks. > > > > We will start the vote sometime this weekend, most likely, unless some > major feedback appears before then, and let the chips fall where they may. > > After working through (most of) the discussion, I've now taken a look at > the updated RFC. I have the following remarks: > > 1. > > > It is really “write-once”, which is not the same as immutable (as shown > above). But there's no reason that “write-once” need be incompatible with > hooks. > > This is a strawman argumentation, as I've outlined in my previous > emails, calling readonly "write-once" is wrong. It is a reasonable user > expectation to always get the identical value when reading a value that > may only be set once. By calling it "write-once" you are trying to shift > the focus to the write operation, which is totally irrelevant for user > expectations when interacting with readonly properties. Especially for > expectations of users that just *use* a class rather than writing one. >
To my ears, write-once is more accurate than readonly because it sticks to the facts of how this behaves. That's very relevant. Using readonly to suggest immutable is where the arguments for rejecting this RFC are weak. readonly doesn't mean immutable, no matter how hard some want it to be...
> > 2. > > > The concern would only appear if someone is deliberately doing something > non-stable > > Or if someone accidentally calls a (deep) function chain that is non-pure. > > 3. > > > Or, for a more real-world and larger example, PHP 8.4 requires this: > > This is false. You are perfectly able to write this in PHP 8.4: > > final readonly class Entry > { > public readonly $terms; > > public function __construct( > public string $word, > public string $slug, > array $terms, > ) { > $this->terms = $this->upcastTerms($terms); > } > > private function upcastTerms(array $terms): array > { > $upcast = static fn (Term|array $term): Term > => $term instanceof Term ? $term : new Term(...$term); > return array_map($upcast, $value) > } > } > > In no way do you need to use a property hook. > > 4. > > > // New code in 8.5: > > > > $p = new PositivePoint(3, 4); > > $p2 = clone($p, ['x' => -10]); > > This is not legal code in PHP 8.5. Clone-with respects visibility and > since your asymmetric visibility RFC included the change, you are > probably aware that `readonly` implies `protected(set)`.
5.
> > > but are now necessary to ensure that invariants are enforced. > > And therefore with PHP 8.5 hooks are not necessary to enforce > invariants, except in the rare case where a `public(set) readonly` > property is used. >
Of course it's rare. It's brand new... Yet this example comes back many times. This is how the community would like to use clone-with. This should be acknowledged. The fact that protected(set) is (currently) the default is not an argument to make public(set) a second class citizen.
> 6. > > > So no guarantees are softened by this RFC. > > Yes, they are. Unless `__get()` is implemented on a class (which is > explicitly visible as part of the public API), readonly guarantees the > immutability of identity. >
Which is not really relevant when talking about immutability. What everybody is looking for when using that word is immutable objects. 7.
> > > While that is an interesting idea that has been floated a few times, it > has enough complexities and edge cases of its own to address that we feel > it is out of scope. > > While it certainly is your right as the RFC authors to consider certain > things out of scope for an RFC, I strongly oppose the notion of shipping > something that is strictly inferior and comes with obvious semantic > issues due to perceived complexity of another solution and then > following up with the proper solution that has already been identified. > As I've outlined in my previous emails, I found defining semantics for > an 'init' hook straight-forward when looking at how PHP works as of today. > > 8. > > > However, this RFC is in no way incompatible with adding an init hook in > the future should it be proposed. > > This is true, but as I've mentioned before, an 'init' hook would enable > the same use cases without bringing along issues. So it really should be > "one of them, but not both" (with "one of them" being the init hook). > > -------- > > After reading through the discussion, it seems the only argument against > the 'init' hook is perceived complexity. It is not at all clear to me > why this means that we must now rush something with clear issues into > PHP 8.5. >
I'd understand the arguments you're pushing for if readonly were appropriate to build immutable objects. Yet that's not the case, so such reasoning is built on sand I'm sorry... To me the RFC enables useful capabilities that authors are going to need. Or find workarounds for. Which means more ugliness to come... Nicolas

Eric Norris

1 year ago
Nick, Larry, On Fri, Jul 18, 2025 at 2:01 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:
> > > > Le ven. 18 juil. 2025 à 18:32, Tim Düsterhus <tim@bastelstu.be> a écrit : >> >> Hi >> >> On 7/17/25 18:26, Larry Garfield wrote: >> > Given the lack of consensus both here and in off-list discussions on how to handle get hooks, we have done the following: >> > >> > * Split the RFC into two sections, one for get, one for set. >> > * Expanded and refined the examples for both. The implementation is still the original, however. >> > * Split the vote into two: one for allowing readonly get hooks, one for readonly set hooks. >> > >> > We will start the vote sometime this weekend, most likely, unless some major feedback appears before then, and let the chips fall where they may. >> >> After working through (most of) the discussion, I've now taken a look at >> the updated RFC. I have the following remarks: >> >> 1. >> >> > It is really “write-once”, which is not the same as immutable (as shown above). But there's no reason that “write-once” need be incompatible with hooks. >> >> This is a strawman argumentation, as I've outlined in my previous >> emails, calling readonly "write-once" is wrong. It is a reasonable user >> expectation to always get the identical value when reading a value that >> may only be set once. By calling it "write-once" you are trying to shift >> the focus to the write operation, which is totally irrelevant for user >> expectations when interacting with readonly properties. Especially for >> expectations of users that just *use* a class rather than writing one. > > > To my ears, write-once is more accurate than readonly because it sticks to the facts of how this behaves. That's very relevant. > Using readonly to suggest immutable is where the arguments for rejecting this RFC are weak. > readonly doesn't mean immutable, no matter how hard some want it to be...
(including a snippet from a separate email from Larry below)
> Does readonly refer to the value returned? If so, that's already been broken since the beginning because the property can be a mutable object, so trusting the data returned to be "the same" is already not safe.
It seems to me that the original intent of `readonly` was to mean immutable, and points to a property always equaling itself in the rationale section (https://wiki.php.net/rfc/readonly_properties_v2#rationale): ``` $prop = $this->prop; $fn(); // Any code may run here. $prop2 = $this->prop; assert($prop === $prop2); // Always holds. ``` It even calls out that this *does not* restrict *interior* mutability, which I believe you are using to argue that it doesn't actually mean immutable: "However, readonly properties do not preclude interior mutability. Objects (or resources) stored in readonly properties may still be modified internally." This is exactly how I think about `readonly`. The identity of property won't change, but the object itself might. Now if the object is also a `readonly` class (and recursive for any of those class's properties that are objects), then you *would* truly have an immutability guarantee of both the identity of the object and the object's properties. This is ignoring __get, which I have pointed out elsewhere is worth ignoring, since we can conceivably remove that from readonly classes if we pass `init` hooks.
> > >> >> 6. >> >> > So no guarantees are softened by this RFC. >> >> Yes, they are. Unless `__get()` is implemented on a class (which is >> explicitly visible as part of the public API), readonly guarantees the >> immutability of identity. > > > Which is not really relevant when talking about immutability. > What everybody is looking for when using that word is immutable objects.
This is not what I am looking for, so I disagree. I would like immutable objects as well, but unless the object itself is a readonly class as noted above, I would not expect it to mean immutable objects.
> > >> 7. >> >> > While that is an interesting idea that has been floated a few times, it has enough complexities and edge cases of its own to address that we feel it is out of scope. >> >> While it certainly is your right as the RFC authors to consider certain >> things out of scope for an RFC, I strongly oppose the notion of shipping >> something that is strictly inferior and comes with obvious semantic >> issues due to perceived complexity of another solution and then >> following up with the proper solution that has already been identified. >> As I've outlined in my previous emails, I found defining semantics for >> an 'init' hook straight-forward when looking at how PHP works as of today. >> >> 8. >> >> > However, this RFC is in no way incompatible with adding an init hook in the future should it be proposed. >> >> This is true, but as I've mentioned before, an 'init' hook would enable >> the same use cases without bringing along issues. So it really should be >> "one of them, but not both" (with "one of them" being the init hook). >> >> -------- >> >> After reading through the discussion, it seems the only argument against >> the 'init' hook is perceived complexity. It is not at all clear to me >> why this means that we must now rush something with clear issues into >> PHP 8.5. > > > I'd understand the arguments you're pushing for if readonly were appropriate to build immutable objects. Yet that's not the case, so such reasoning is built on sand I'm sorry... > > To me the RFC enables useful capabilities that authors are going to need. Or find workarounds for. Which means more ugliness to come...
I am failing to understand what capabilities are not going to be addressed by an `init` hook, which some of us (if I'm allowed to speak for us) seem to think is the correct approach here. I have noticed in some discussions with my coworkers that it seems that some people think that readonly implies a *contract about writability to consumers of the class*, that is, it implies that consumers of the class can only *read* the value, not write it. I think I could understand why people that think this way would have no problem with `get` hooks, since it still upholds what they think the contract is. I feel, however, that that desired contract is actually achieved by asymmetric visibility, e.g. public(get) protected(set), which I view as orthogonal to readonly. When I point out that asymmetric visibility exists, everyone I've talked to so far agrees that readonly makes more sense as a *contract that the value won't change*. And, as I've pointed out above, I believe that is the intent of the original RFC text for the feature. With this understanding of the contract of readonly, I struggle to understand why an author *who wants to use readonly* would need `get` hook capabilities and not in fact `init` hook capabilities. If they needed generic `get` hook capabilities for a property (and not just lazy loading), then my position is that *they don't actually want readonly*.

Rob Landers

1 year ago
On Fri, Jul 18, 2025, at 21:43, Eric Norris wrote:
> Nick, Larry, > > On Fri, Jul 18, 2025 at 2:01 PM Nicolas Grekas > <nicolas.grekas+php@gmail.com <mailto:nicolas.grekas%2Bphp@gmail.com>> wrote: > > > > > > > > Le ven. 18 juil. 2025 à 18:32, Tim Düsterhus <tim@bastelstu.be> a écrit : > >> > >> Hi > >> > >> On 7/17/25 18:26, Larry Garfield wrote: > >> > Given the lack of consensus both here and in off-list discussions on how to handle get hooks, we have done the following: > >> > > >> > * Split the RFC into two sections, one for get, one for set. > >> > * Expanded and refined the examples for both. The implementation is still the original, however. > >> > * Split the vote into two: one for allowing readonly get hooks, one for readonly set hooks. > >> > > >> > We will start the vote sometime this weekend, most likely, unless some major feedback appears before then, and let the chips fall where they may. > >> > >> After working through (most of) the discussion, I've now taken a look at > >> the updated RFC. I have the following remarks: > >> > >> 1. > >> > >> > It is really “write-once”, which is not the same as immutable (as shown above). But there's no reason that “write-once” need be incompatible with hooks. > >> > >> This is a strawman argumentation, as I've outlined in my previous > >> emails, calling readonly "write-once" is wrong. It is a reasonable user > >> expectation to always get the identical value when reading a value that > >> may only be set once. By calling it "write-once" you are trying to shift > >> the focus to the write operation, which is totally irrelevant for user > >> expectations when interacting with readonly properties. Especially for > >> expectations of users that just *use* a class rather than writing one. > > > > > > To my ears, write-once is more accurate than readonly because it sticks to the facts of how this behaves. That's very relevant. > > Using readonly to suggest immutable is where the arguments for rejecting this RFC are weak. > > readonly doesn't mean immutable, no matter how hard some want it to be... > > (including a snippet from a separate email from Larry below) > > > Does readonly refer to the value returned? If so, that's already been broken since the beginning because the property can be a mutable object, so trusting the data returned to be "the same" is already not safe. > > It seems to me that the original intent of `readonly` was to mean > immutable, and points to a property always equaling itself in the > rationale section > (https://wiki.php.net/rfc/readonly_properties_v2#rationale): > > ``` > $prop = $this->prop; > $fn(); // Any code may run here. > $prop2 = $this->prop; > assert($prop === $prop2); // Always holds. > ``` > > It even calls out that this *does not* restrict *interior* mutability, > which I believe you are using to argue that it doesn't actually mean > immutable: > > "However, readonly properties do not preclude interior mutability. > Objects (or resources) stored in readonly properties may still be > modified internally." > > This is exactly how I think about `readonly`. The identity of property > won't change, but the object itself might. Now if the object is also a > `readonly` class (and recursive for any of those class's properties > that are objects), then you *would* truly have an immutability > guarantee of both the identity of the object and the object's > properties. This is ignoring __get, which I have pointed out elsewhere > is worth ignoring, since we can conceivably remove that from readonly > classes if we pass `init` hooks.
I don't want to accuse you of cherry-picking ... but this is clearly cherry picking. From that same text: --- It is worth noting that having a readonly property feature does not preclude introduction of accessors. C# supports both readonly properties and accessors. C# also provides properties with implicit backing storage through accessor syntax, but this is not the only way to do it. For example, Swift has special syntax for asymmetric visibility, rather than specifying visibility on implicitly implemented accessors. Even if we have property accessors, I believe it may be worthwhile to limit them to computed properties only, and solve use-cases that involve engine-managed storage through other mechanisms, such as readonly properties and property-level asymmetric visibility. This avoids confusion relating to the two kinds of accessors (implicit and explicit), and also allows us to make their behavior independent of accessor constraints. For example, a first-class asymmetric visibility feature would shield the user from considering distinctions such as `get;` vs `&get;` accessors. These are externalities of the general accessor feature and not needed for asymmetric visibility. A separate implementation can also be more efficient. After initialization, a readonly property will have the same performance characteristics as a normal property. Accessor-based properties, even with implicit storage, still carry a performance penalty. --- The original author (Nikita) suggested that there's nothing in the original design that precludes accessors -- and highlights languages where there are both and they are doing just fine more than 5 years later. [snip]
> I am failing to understand what capabilities are not going to be > addressed by an `init` hook, which some of us (if I'm allowed to speak > for us) seem to think is the correct approach here. > > I have noticed in some discussions with my coworkers that it seems > that some people think that readonly implies a *contract about > writability to consumers of the class*, that is, it implies that > consumers of the class can only *read* the value, not write it. I > think I could understand why people that think this way would have no > problem with `get` hooks, since it still upholds what they think the > contract is. I feel, however, that that desired contract is actually > achieved by asymmetric visibility, e.g. public(get) protected(set), > which I view as orthogonal to readonly. > > When I point out that asymmetric visibility exists, everyone I've > talked to so far agrees that readonly makes more sense as a *contract > that the value won't change*. And, as I've pointed out above, I > believe that is the intent of the original RFC text for the feature. > > With this understanding of the contract of readonly, I struggle to > understand why an author *who wants to use readonly* would need `get` > hook capabilities and not in fact `init` hook capabilities. If they > needed generic `get` hook capabilities for a property (and not just > lazy loading), then my position is that *they don't actually want > readonly*. >
I think an init hook is out of the question for 8.5, so I'm not even sure it's worth discussing. — Rob

Claude Pache

1 year ago
> Le 19 juil. 2025 à 00:41, Rob Landers <rob@bottled.codes> a écrit : > > The original author (Nikita) suggested that there's nothing in the original design that precludes accessors -- and highlights languages where there are both and they are doing just fine more than 5 years later.
Hi Rob, It is indeed entirely reasonable to have both readonly properties and hooked properties (aka accessors), and today PHP has indeed both of them (and even asymmetric visibility on top of that, as a separate feature contrarily to C#). But it doesn’t mean that it is reasonable for the *same* property to be *both* readonly and hooked, which is the point that is currently disputed. — What do the other languages allow? Is it possible to define a readonly property with a user-defined getter? (Disclaimer: Even if one of them allows such a thing, I’ll still think that it is a bad idea.) —Claude

Rob Landers

1 year ago
On Sat, Jul 19, 2025, at 03:04, Claude Pache wrote:
> > > >> Le 19 juil. 2025 à 00:41, Rob Landers <rob@bottled.codes> a écrit : >> >> The original author (Nikita) suggested that there's nothing in the original design that precludes accessors -- and highlights languages where there are both and they are doing just fine more than 5 years later. >> > > Hi Rob, > > It is indeed entirely reasonable to have both readonly properties and hooked properties (aka accessors), and today PHP has indeed both of them (and even asymmetric visibility on top of that, as a separate feature contrarily to C#). But it doesn’t mean that it is reasonable for the *same* property to be *both* readonly and hooked, which is the point that is currently disputed. — What do the other languages allow? Is it possible to define a readonly property with a user-defined getter? (Disclaimer: Even if one of them allows such a thing, I’ll still think that it is a bad idea.) > > —Claude
Hey Claude, From what I've seen in other languages, this combination is fairly common and not inherently poblematic. - C# allows get hooks with user-defined logic, even in readonly structs. - Kotlin uses val with a get() body, which is readonly from the consumer's perspective, even though the value is computed. - TypeScript allows a get-only accessor which acts readonly. - Swift also allows get-only computed accessors/hooks. Most languages treat these as an abstraction boundary: allowing you to expose computed state while still guaranteeing external immutability. This RFC is proposing something similar: the public surface is observably immutable, even if a value is derived. We can already do this today with more boilerplate: class Foo { public function __construct(private readonly int $_bar) {} public int $bar { get => $this->_bar * 2; } } The RFC reduces that boilerplate and clarifies intent. In the example above, there is nothing mutable about it. It is "read only" from a public contract point-of-view, we just can't mark it as a readonly class. As for side-effects or non-determinism, that is a valid concern, but one that applies to any property hook and non-scalar property. A readonly declaration doesn't necessarily encourage or prevent that, it simply declares that the instance state cannot be mutated after construction. I'd argue the RFC aligns well with the conventions and capabilities of other languages, and builds on what PHP already allows. — Rob

Claude Pache

1 year ago
> Le 19 juil. 2025 à 09:46, Rob Landers <rob@bottled.codes> a écrit : > > > > On Sat, Jul 19, 2025, at 03:04, Claude Pache wrote: >> >> >> >>> Le 19 juil. 2025 à 00:41, Rob Landers <rob@bottled.codes> a écrit : >>> >>> The original author (Nikita) suggested that there's nothing in the original design that precludes accessors -- and highlights languages where there are both and they are doing just fine more than 5 years later. >>> >> >> Hi Rob, >> >> It is indeed entirely reasonable to have both readonly properties and hooked properties (aka accessors), and today PHP has indeed both of them (and even asymmetric visibility on top of that, as a separate feature contrarily to C#). But it doesn’t mean that it is reasonable for the *same* property to be *both* readonly and hooked, which is the point that is currently disputed. — What do the other languages allow? Is it possible to define a readonly property with a user-defined getter? (Disclaimer: Even if one of them allows such a thing, I’ll still think that it is a bad idea.) >> >> —Claude > > Hey Claude, > > From what I've seen in other languages, this combination is fairly common and not inherently poblematic. > > - C# allows get hooks with user-defined logic, even in readonly structs. > - Kotlin uses val with a get() body, which is readonly from the consumer's perspective, even though the value is computed. > - TypeScript allows a get-only accessor which acts readonly. > - Swift also allows get-only computed accessors/hooks.
Hi Rob, The main problem is that we don’t agree on the meaning of “readonly property”. I’ve check TypeScript: * It has getters and setters, which correspond to PHP get/set hooks without backing store. * Separately, it also has a `readonly` modifier that can be applied to properties; the semantics is that such a property may be initialised either at declaration or inside the constructor, but cannot be modified afterwards. That corresponds approximatively to PHP readonly properties. * But a “get-only accessor” is not the same thing as a “readonly property” in the specific sense of ”a property decorated with the `readonly` modifier”. Also, you cannot add the readonly modifier to a get accessor. —Claude

Rob Landers

1 year ago
On Sat, Jul 19, 2025, at 12:09, Claude Pache wrote:
> > >> Le 19 juil. 2025 à 09:46, Rob Landers <rob@bottled.codes> a écrit : >> >> >> >> On Sat, Jul 19, 2025, at 03:04, Claude Pache wrote: >>> >>> >>> >>>> Le 19 juil. 2025 à 00:41, Rob Landers <rob@bottled.codes> a écrit : >>>> >>>> The original author (Nikita) suggested that there's nothing in the original design that precludes accessors -- and highlights languages where there are both and they are doing just fine more than 5 years later. >>>> >>> >>> Hi Rob, >>> >>> It is indeed entirely reasonable to have both readonly properties and hooked properties (aka accessors), and today PHP has indeed both of them (and even asymmetric visibility on top of that, as a separate feature contrarily to C#). But it doesn’t mean that it is reasonable for the *same* property to be *both* readonly and hooked, which is the point that is currently disputed. — What do the other languages allow? Is it possible to define a readonly property with a user-defined getter? (Disclaimer: Even if one of them allows such a thing, I’ll still think that it is a bad idea.) >>> >>> —Claude >> >> Hey Claude, >> >> From what I've seen in other languages, this combination is fairly common and not inherently poblematic. >> >> - C# allows get hooks with user-defined logic, even in readonly structs. >> - Kotlin uses val with a get() body, which is readonly from the consumer's perspective, even though the value is computed. >> - TypeScript allows a get-only accessor which acts readonly. >> - Swift also allows get-only computed accessors/hooks. > > Hi Rob, > > The main problem is that we don’t agree on the meaning of “readonly property”. > > I’ve check TypeScript: > > * It has getters and setters, which correspond to PHP get/set hooks without backing store. > > * Separately, it also has a `readonly` modifier that can be applied to properties; the semantics is that such a property may be initialised either at declaration or inside the constructor, but cannot be modified afterwards. That corresponds approximatively to PHP readonly properties. > > * But a “get-only accessor” is not the same thing as a “readonly property” in the specific sense of ”a property decorated with the `readonly` modifier”. Also, you cannot add the readonly modifier to a get accessor. > > —Claude >
The error you get when trying to modify the "get-only accessor" is `Error: Cannot assign to 'name' because it is a read-only property` Thus, readonly is implied by the get-only accessor, there's no need to specify it directly. — Rob

Larry Garfield

1 year ago
On Fri, Jul 18, 2025, at 11:29 AM, Tim Düsterhus wrote:
>> // New code in 8.5: >> >> $p = new PositivePoint(3, 4); >> $p2 = clone($p, ['x' => -10]); > > This is not legal code in PHP 8.5. Clone-with respects visibility and > since your asymmetric visibility RFC included the change, you are > probably aware that `readonly` implies `protected(set)`. > > 5. > >> but are now necessary to ensure that invariants are enforced. > > And therefore with PHP 8.5 hooks are not necessary to enforce > invariants, except in the rare case where a `public(set) readonly` > property is used.
This is a valid point, thanks. I have updated the example to use `public(set)`, and note that it means many withX() methods can be eliminated, as their restrictions can be placed on the property instead. Note to self: public(clone) ...
> 6. > >> So no guarantees are softened by this RFC. > > Yes, they are. Unless `__get()` is implemented on a class (which is > explicitly visible as part of the public API), readonly guarantees the > immutability of identity.
Which would also be guaranteed with the "refers to the backing value" proposal, which you also rejected. Does readonly refer to the physical value (backing value, object identity) or the "value returned"? That wasn't a distinction that mattered in 8.1 when readonly was introduced, though now it does. If yes, then all that needs to be protected is the backing value, so get hooks that respect that should be fine. Does readonly refer to the value returned? If so, that's already been broken since the beginning because the property can be a mutable object, so trusting the data returned to be "the same" is already not safe. At a conceptual level, we need to decide what readonly means. There is clearly no consensus on that right now, and that's going to cause problems any time we touch readonly, not just this RFC.
>> While that is an interesting idea that has been floated a few times, it has enough complexities and edge cases of its own to address that we feel it is out of scope. > > While it certainly is your right as the RFC authors to consider certain > things out of scope for an RFC, I strongly oppose the notion of shipping > something that is strictly inferior and comes with obvious semantic > issues due to perceived complexity of another solution and then > following up with the proper solution that has already been identified. > As I've outlined in my previous emails, I found defining semantics for > an 'init' hook straight-forward when looking at how PHP works as of today.
I suppose this is a valid argument, given that readonly was implemented because Nikita felt the proper solution of full aviz would be too complex so went with the simpler solution, which, turns out, is strictly inferior and comes with obvious semantic issues.
> After reading through the discussion, it seems the only argument against > the 'init' hook is perceived complexity. It is not at all clear to me > why this means that we must now rush something with clear issues into > PHP 8.5.
At this point, I fully expect the 'get' part of the RFC to not pass. Someone else is welcome to then work on an init hook. The seemingly less controversial 'set' hook still has considerable value on its own, and hopefully that passes. --Larry Garfield