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
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 allowreadonly
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
Hey Volker,
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
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
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
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 properreadonly
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 thereadonly
-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
Hi Larry et al.
Le mar. 8 juil. 2025 à 17:12, Larry Garfield larry@garfieldtech.com 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 properreadonly
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 thereadonly
-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.
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:
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
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: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 aninit
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
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:
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
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:
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)
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-strictLarry and I agree that we don’t want this complexity in the current RFC.
Perhaps this is something for a separateinit
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
Hi
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
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: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 aninit
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:
-
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 bewriteonce
, because that's all it is. (Once again, this is likely the most poorly designed feature we've added in many years.) -
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, becausereadonly
-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
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.
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 aswriteonce
, which makes sense,
but then why would aninit
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 allowingset
hooks (everyone seems +1 to that), aninit
hook, and disallowing
get
hooks. It would sidestep the controversial nature of aget
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 potentialreadonly
engine optimizations like Tim pointed out. Once we allowget
hooks,
there's no going back. If we still needed to addget
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
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 aswriteonce
, which makes sense,
but then why would aninit
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 allowingset
hooks (everyone seems +1 to that), aninit
hook, and disallowing
get
hooks. It would sidestep the controversial nature of aget
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 potentialreadonly
engine optimizations like Tim pointed out. Once we allowget
hooks,
there's no going back. If we still needed to addget
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?
I apologize for being ignorant here, but I'm not sure what you mean by
"referencing itself". Do you mean the backing value? No, because the
backing value is unset - this is the initialization (and in the case
of readonly, this is the "writeonce").
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?
I don't have strong feelings here, but I think this is solvable in a
way that is consistent and unsurprising. If you set the value, the set
hook triggers, if the set hook reads the value, it triggers the init
hook, if it's readonly, this might trigger an error since the set hook
would try to set it a second time.
Can you have both an init hook and a get hook? What happens then?
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?
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.
I guess I don't agree that this is a reasonable solution at the
moment. I don't know that I think it's unreasonable, either, but
something about it feels wrong to me. I agree with Tim that I think of
readonly properties as guaranteeing the immutability of identity.
Regarding the caching option suggested elsewhere, the semantics
mentioned seem confusing to me. The body is called on subsequent gets,
but only the value from the first get is returned? I would expect that
would be very confusing to developers. If the body wasn't executed on
subsequent invocations that might make more sense to me, but then the
get hook essentially is an init hook, and why not call it as such.
Also, just to throw the idea out there - maybe start with init hooks
only for readonly properties? Is there something to this, especially
if you consider "readonly" actually "writeonce"? Maybe this plus
disallowing set hooks?
Hey Eric,
Regarding the caching option suggested elsewhere, the semantics
mentioned seem confusing to me. The body is called on subsequent gets,
but only the value from the first get is returned? I would expect that
would be very confusing to developers. If the body wasn't executed on
subsequent invocations that might make more sense to me, but then the
get hook essentially is an init hook, and why not call it as such.
I just before pushed a third implementation idea which addresses this.
It also no longer requires a separate cache. It uses the store itself.
A link to the branch is in the description of the original PR.
Also, just to throw the idea out there - maybe start with init hooks
only for readonly properties? Is there something to this, especially
if you consider "readonly" actually "writeonce"? Maybe this plus
disallowing set hooks?
Personally, I don’t see added value in adding yet another hook init
.
I also don’t see how this would solve the get
hook issue?
As a user I want to be able to have a get
hook in readonly classes. For instance, for formatting.
In my last answer to Tim I showed an example of what I would expect from readonly
properties.
Cheers,
Nick
Hi
see also my previous reply to your email where you initially mentioned
init hooks.
Can an init hook reference itself, the way get and set can?
If the init hook references its own property you will get endless recursion.
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?
The first bit I answered in my previous email. If set reads from itself
and the property is uninitialized, it will invoke the init hook.
Should combining init and set be forbidden as confusing?
No.
Can you have both an init hook and a get hook? What happens then?
See previous email.
Repeat all of the above on readonly properties.
No difference, except that the 'get' hook should not be allowed there.
there is a reasonable solution right in front of us that is trivial to implement.
I disagree on the "reasonable" part.
Best regards
Tim Düsterhus
Hi
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 bewriteonce
, 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.
- 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, becausereadonly
-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
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 bewriteonce
, 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.
- 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, becausereadonly
-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 ofreadonly
by allowing aget
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
Hi
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.
Yes. The point is that the semantics users expect from readonly
are
fundamentally incompatible with a get hook that could return arbitrarily
changing values whenever you read from a property.
Proposing to change this is like proposing an RFC that allows storing a
string
in a property with type array
. It would be a massive break in
user expectations for a feature that existed since 4 PHP versions (in
case of readonly).
Best regards
Tim Düsterhus
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.Yes. The point is that the semantics users expect from
readonly
are
fundamentally incompatible with a get hook that could return arbitrarily
changing values whenever you read from a property.
Just a heads up: I also plan to vote "no" on this RFC because the
expectation with readonly
is that there is no kind of interference or
lazy initialization anyway.
Now that lazy proxies have landed into core, there is also no need for
__get
hacks anymore.
Marco Pivetta
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.Yes. The point is that the semantics users expect from
readonly
are
fundamentally incompatible with a get hook that could return arbitrarily
changing values whenever you read from a property.Just a heads up: I also plan to vote "no" on this RFC because the
expectation withreadonly
is that there is no kind of interference or
lazy initialization anyway.Now that lazy proxies have landed into core, there is also no need for
__get
hacks anymore.
Even on set hooks, which do not violate any of the interpretations of what readonly "means" floating about? As the RFC notes, that's now a necessary validation step with the improved clone() on its way.
--Larry Garfield
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
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
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üsterhusNick 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,
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); // 42In 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
, orgetAccessLevel
-- 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:
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
Hey Rob,
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); // 42In 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
, orgetAccessLevel
-- 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: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
Hey Nick,
I was merely illustrating the different types, you can simply replace them with non-virtual examples. To your idea of forced memoization, I'm not sure that's a good idea. In cases where its a single execution context per request, it may be fine, but in cases of worker mode on frankenphp, where readonly objects may live for quite awhile, it may not be. Generally, memoization is an optimization, not a property of a value.
In your example above, this breaks the principle of least astonishment, and potentially violates referential transparency due to the calculation being non-pure. Referential transparency basically says we can can replace the use of the variable with it's value (in this case, $ususual->value with $unusual->backing_value * random_int(1, 100)). If it is memoized, we cannot. There isn't a way to express that it is one value the first time (random) and a different value the next time (the previous computation). This is a bit dangerous because the programmer has no way to reliably reason about the code as if the expression can be substituted freely.
— 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: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
Hey Nick,
I was merely illustrating the different types, you can simply replace them with non-virtual examples. To your idea of forced memoization, I'm not sure that's a good idea. In cases where its a single execution context per request, it may be fine, but in cases of worker mode on frankenphp, where readonly objects may live for quite awhile, it may not be. Generally, memoization is an optimization, not a property of a value.
In your example above, this breaks the principle of least astonishment, and potentially violates referential transparency due to the calculation being non-pure. Referential transparency basically says we can can replace the use of the variable with it's value (in this case, $ususual->value with $unusual->backing_value * random_int(1, 100)). If it is memoized, we cannot. There isn't a way to express that it is one value the first time (random) and a different value the next time (the previous computation). This is a bit dangerous because the programmer has no way to reliably reason about the code as if the expression can be substituted freely.
— Rob
Hey Rob,
-
Well, we cannot have it both in the same time. Either we want a strict guarantee that the value doesn’t change after the first
get
hook call on areadonly
property, or we want the opposite. So the proposed solution now is to be strict by default. If you want it non-cached you can use nonreadonly
. -
The same point could be made outside of "readonly hooks", for everything with any kind of randomness or external state (database, filesystem, …) source. That’s the trade-off.
I find it very acceptable as a solution for the concern brought up by Claude and Tim.
Cheers,
Nick
Aside: I just learned about your “records” RFC. Good stuff!
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üsterhusNick 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); // 42In 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
, orgetAccessLevel
-- 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:
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
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üsterhusNick 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); // 42In 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
, orgetAccessLevel
-- 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 afterget
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: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 withreadonly
.
Alternatively, as you proposed, using methods (which I think would really be a better fit; alternatively virtual properties which also will not supportreadonly
.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
Hey Nick,
After sleeping on it, I think I agree with this assessment. For backed properties, especially, it makes sense that the value feels "immutable". If and when we get to virtual properties, maybe not. But that's a bridge to cross later.
— Rob
Hi
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
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
Hey Tim,
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:
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.
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.
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.
Personally, I would really like to have
get
hooks on readonly properties. Please consider something like this: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
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
Op zaterdag 19 juli 2025 schreef Rob Landers rob@bottled.codes:
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
Op zaterdag 19 juli 2025 schreef Rob Landers rob@bottled.codes:
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
Hey Niels,
In before: I personally didn’t have a problem with Tim joining in late.
I, however, want to express that I don’t feel anything productive happens at this point.
My personal impression is, there wasn’t anything new added since the early beginning of the discussion.
Everything was brought up early on. If you believe that’s not the case and I missed something, please point me to the new arguments.
So, calling it “not wound down” feels off to me. And that’s why I answer now.
As someone who is a new participant here, please allow me to ask:
When is a discussion allowed to be considered wound down?
And, is repeating the same arguments (just by different persons) really a reason to keep a discussion going?
The whole controversy is about get
.
We addressed this by switching to a split vote, because literally all those concerns/opinions (allow it; don’t allow it; add init
instead; cache it; don’t cache it) can for now be “addressed” with a “no”-vote on get
, and then follow up with a new get
RFC later.
Am I wrong?
What is this all about now?
What are we doing now?
Do we keep repeating the same arguments, and disallow bringing this to vote at all? Even though like literally everyone seems to be on board with “set
is ok”?
Or are we allowed to move on with a vote?
despite there being no clear consensus
The clear consensus seems to be that set
should be allowed. That’s why we adjusted the vote.
I repeat: everyone with a problem (any kind) on get
can vote “no” on get
and “yes” on set
.
—
Again, this is nothing specifically towards Tim.
Cheers,
Nick
Hey Niels,
Hey Nick
In before: I personally didn’t have a problem with Tim joining in late.
I, however, want to express that I don’t feel anything productive happens at this point.
My personal impression is, there wasn’t anything new added since the early beginning of the discussion.
Everything was brought up early on. If you believe that’s not the case and I missed something, please point me to the new arguments.
No new arguments came as the concerns have been sent out earlier indeed.
Lately the discussion seems more geared towards the expectations of the users of readonly, and what that means for the set hook.
So, calling it “not wound down” feels off to me. And that’s why I answer now.
As someone who is a new participant here, please allow me to ask:
When is a discussion allowed to be considered wound down?
This has never been defined and always has a subjective part.
In general, no one will block you from bringing it to a vote if no substantial changes have been made to the RFC and the discussion lasted at least 2 weeks.
And, is repeating the same arguments (just by different persons) really a reason to keep a discussion going?
No, but it isn't bad to point out some things right before the vote.
The whole controversy is about
get
.
That's true.
Note though that the fact that the RFC still includes this does show a non-consensus from the authors PoV.
Either you fully stand behind your own RFC and wouldn't have split the vote, or you agreed that the get hook is a bad idea.
In the latter case, why even include this still in the RFC text, especially after Larry said he's positive that part won't pass?
This comes across as really wanting something of the RFC to pass, not aiming for the best "solution".
We addressed this by switching to a split vote, because literally all those concerns/opinions (allow it; don’t allow it; add
init
instead; cache it; don’t cache it) can for now be “addressed” with a “no”-vote onget
, and then follow up with a newget
RFC later.
It's important to plan for the future and come up with a holistic solution.
I don't want to end up in a situation where in hindsight we shouldn't have allowed a "set hook" for example and should've just left readonly alone.
Am I wrong?
What is this all about now?
What are we doing now?
Do we keep repeating the same arguments, and disallow bringing this to vote at all? Even though like literally everyone seems to be on board with “
set
is ok”?
I believe I answered this, but just to make it extra clear: you are allowed to bring this to a vote, no one can veto you for that.
I don't believe everyone (who replied) is pro "set hook". And definitely not in other channels.
Or are we allowed to move on with a vote?
despite there being no clear consensus
The clear consensus seems to be that
set
should be allowed. That’s why we adjusted the vote.I repeat: everyone with a problem (any kind) on
get
can vote “no” onget
and “yes” onset
.
I believe I answered this above already.
—
Again, this is nothing specifically towards Tim.
Cheers,
Nick
I'll end with saying that this should not discourage you from interacting with the ML.
You got a bit "unlucky" with the subjects you chose as both hooks and readonly are a bit controversial topics to begin with.
Kind regards
Niels
Hey Niels,
Hey Nick
Hey Nils,
The whole controversy is about
get
.That's true.
Note though that the fact that the RFC still includes this does show a non-consensus from the authors PoV.
Either you fully stand behind your own RFC and wouldn't have split the vote, or you agreed that the get hook is a bad idea.
This is a misinterpretation.
In the latter case, why even include this still in the RFC text, especially after Larry said he's positive that part won't pass?
This comes across as really wanting something of the RFC to pass, not aiming for the best "solution".
Yes, we really want the set
part to pass.
No, this does not mean that we not aim for the best solution.
We listened to feedback and adjusted the voting structure accordingly.
Please don’t forget that some people here support the RFC as is.
As far as my understanding goes the list discussion is not a pre-vote.
The vote will show what the majority, including the silent part, wants.
There is, in my opinion, no need to patronise and prevent them from voting for what they want.
We addressed this by switching to a split vote, because literally all those concerns/opinions (allow it; don’t allow it; add
init
instead; cache it; don’t cache it) can for now be “addressed” with a “no”-vote onget
, and then follow up with a newget
RFC later.It's important to plan for the future and come up with a holistic solution.
I don't want to end up in a situation where in hindsight we shouldn't have allowed a "set hook" for example and should've just left readonly alone.
I honestly cannot come up with a reason for why this would be the case.
Am I wrong?
What is this all about now?
What are we doing now?
Do we keep repeating the same arguments, and disallow bringing this to vote at all? Even though like literally everyone seems to be on board with “
set
is ok”?I believe I answered this, but just to make it extra clear: you are allowed to bring this to a vote, no one can veto you for that.
I don't believe everyone (who replied) is pro "set hook". And definitely not in other channels.
You asked to seek “clear consensus”. I am and was happy to take all opinions into account. So, I think I did that.
I, naturally, cannot take any opinions into account I am not aware of because they were expressed in “other channels”.
Here, on-list, everyone seems to be on board. And that’s what I can work with.
I'll end with saying that this should not discourage you from interacting with the ML.
I appreciate the consideration! Everything is ok.
Not succeeding will not hurt my feelings, and I’ll stick around.
You got a bit "unlucky" with the subjects you chose as both hooks and readonly are a bit controversial topics to begin with.
In retrospective I am very aware of that! 😅
Kind regards
Niels
Cheers,
Nick
Hey all,
It's important to plan for the future and come up with a holistic solution.
I don't want to end up in a situation where in hindsight we shouldn't have allowed a "set hook" for example and should've just left readonly alone.I honestly cannot come up with a reason for why this would be the case.
@Niels
I saw you voted “no” for set
.
I double checked the full RFC discussion. You didn’t participate at all until the very end.
Both mails were on a meta-level. None one your mails had any arguments which would justify your vote.
I don’t understand your vote. Would you mind to elaborate?
@Tim
I saw you voted “no” for get
(expected, and understandable), and decided to be abstinent to set
.
I double checked the full RFC discussion.
You asked four times explicitly, and one time indirectly, to allow set
but not get
.
Would you mind to elaborate why you decided to not vote “yes” for what you asked for?
—
As a new participant, I have difficulties to understand these kind of “politics” here.
What was this six weeks discussion exactly for if decisions apparently are taken in “other channels” that are not the officially documented ones?
Thank you.
Cheers,
Nick
@Niels
I saw you voted “no” forset
.
I double checked the full RFC discussion. You didn’t participate at all until the very end.
Both mails were on a meta-level. None one your mails had any arguments which would justify your vote.
I don’t understand your vote. Would you mind to elaborate?
Hi Nick
The vote for me was lost at the proposal itself, not the discussion.
Property hooks left a bit of a bitter aftertaste for me, we're still fixing opcache bugs regarding property hooks today,
and we had a particularly bad bug with the JIT where the JIT made some assumptions that were broken because hooks could override properties.
We also regularly get reports on php-src of people being confused by some of the behaviours of hooks.
Combine that with readonly, another complex feature, and I find it hard to be in favour of any of this.
While I think it's still a great feature, it also proved to be much more complex than initially thought, and it also shows that features
interact in unexpected ways with each other.
For me, the mental model of readonly is already complicated, and the hooks are too, combine the two and you get IMO unintuitive behaviour
regarding immutability etc. Anyway, this is not a new argument.
As a new participant, I have difficulties to understand these kind of “politics” here.
For me there is no politics at play regarding my vote.
For a large chunk of RFCs, I actually don't even vote.
For this one, I decided long ago.
What was this six weeks discussion exactly for if decisions apparently are taken in “other channels” that are not the officially documented ones?
The decision for me was made on my own.
There was one brief discussion in the PHPF Slack that Larry started, I briefly joined in there and there I told him about the mental model issue that I described above.
A few others also were not happy with the complicated mental model.
Thank you.
Cheers,
Nick
Kind regards
Niels
Hey Niels,
@Niels
I saw you voted “no” forset
.
I double checked the full RFC discussion. You didn’t participate at all until the very end.
Both mails were on a meta-level. None one your mails had any arguments which would justify your vote.
I don’t understand your vote. Would you mind to elaborate?Hi Nick
The vote for me was lost at the proposal itself, not the discussion.
Property hooks left a bit of a bitter aftertaste for me, we're still fixing opcache bugs regarding property hooks today,
and we had a particularly bad bug with the JIT where the JIT made some assumptions that were broken because hooks could override properties.
We also regularly get reports on php-src of people being confused by some of the behaviours of hooks.
Combine that with readonly, another complex feature, and I find it hard to be in favour of any of this.
While I think it's still a great feature, it also proved to be much more complex than initially thought, and it also shows that features
interact in unexpected ways with each other.For me, the mental model of readonly is already complicated, and the hooks are too, combine the two and you get IMO unintuitive behaviour
regarding immutability etc. Anyway, this is not a new argument.Thank you.
Cheers,
NickKind regards
Niels
Thanks for the detailed answer--I get it.
I honestly would have appreciated to see you hop in with that in the earlier state of the RFC.
It would have been more productive from my point of view. Maybe next time!
As a new participant, I have difficulties to understand these kind of “politics” here.
For me there is no politics at play regarding my vote.
For a large chunk of RFCs, I actually don't even vote.
For this one, I decided long ago.What was this six weeks discussion exactly for if decisions apparently are taken in “other channels” that are not the officially documented ones?
The decision for me was made on my own.
There was one brief discussion in the PHPF Slack that Larry started, I briefly joined in there and there I told him about the mental model issue that I described above.
A few others also were not happy with the complicated mental model.
Neve mind me then. The “other channels “ argument sounded weird to me because I had no idea what it means. My bad.
--
Thank you.
Cheers,
Nick
Thanks for the detailed answer--I get it.
I honestly would have appreciated to see you hop in with that in the earlier state of the RFC.
It would have been more productive from my point of view. Maybe next time!
Hi Nick.
Honestly, it would've been extremely challenging to convince me anyway...
In any case, I'll make a mental note for that to put this argument out more upfront earlier.
As a new participant, I have difficulties to understand these kind of “politics” here.
For me there is no politics at play regarding my vote.
For a large chunk of RFCs, I actually don't even vote.
For this one, I decided long ago.What was this six weeks discussion exactly for if decisions apparently are taken in “other channels” that are not the officially documented ones?
The decision for me was made on my own.
There was one brief discussion in the PHPF Slack that Larry started, I briefly joined in there and there I told him about the mental model issue that I described above.
A few others also were not happy with the complicated mental model.Neve mind me then. The “other channels “ argument sounded weird to me because I had no idea what it means. My bad.
I guess I should've been clearer. We're good though.
In any case, good luck with any future endeavors in PHP(-src).
--
Thank you.
Cheers,
Nick
Kind regards
Niels
Hi
Am 2025-07-21 10:29, schrieb Nick:
I saw you voted “no” for
get
(expected, and understandable), and
decided to be abstinent toset
.
I double checked the full RFC discussion.
You asked four times explicitly, and one time indirectly, to allow
set
but notget
.
Would you mind to elaborate why you decided to not vote “yes” for what
you asked for?
I don't think I did ask for set(). I said I didn't see (obvious)
problems with set(), in other words I said that I wasn't against it. See
also https://news-web.php.net/php.internals/128123.
Best regards
Tim Düsterhus
When is a discussion allowed to be considered wound down?
This has never been defined and always has a subjective part.
In general, no one will block you from bringing it to a vote if no
substantial changes have been made to the RFC and the discussion lasted
at least 2 weeks.
Indeed, and I've been yelled at in the past for making non-trivial changes to an RFC "too close" to when the vote is called. (For some undefined definition of "too close.")
Note though that the fact that the RFC still includes this does show a
non-consensus from the authors PoV.
Either you fully stand behind your own RFC and wouldn't have split the
vote, or you agreed that the get hook is a bad idea.
In the latter case, why even include this still in the RFC text,
especially after Larry said he's positive that part won't pass?
This comes across as really wanting something of the RFC to pass, not
aiming for the best "solution".
No, we split the vote because, as stated, based on the available evidence (this list) "set" appears to be uncontroversial, but "get" is. But ripping out half the RFC right before calling a vote would certainly qualify as a not-small change, and if we want to get the "set" portion into 8.5, we have a short window before a vote can be called. Hence, splitting the vote, which is the smaller change. Submitting essentially a new RFC at this point isn't really an option.
If by some miracle the 'get' hook also passes, I'm OK with that. But it's hardly the first vote that's been started despite considerable opposition.
I do not recall seeing anyone make a compelling argument against readonly 'set' hooks, so at the moment it does look to us like allowing set hooks, at least, is a "best solution." (Or at least, best presented so far.)
The idea that a split vote means the authors don't support their own RFC is nonsensical, given that multi-part votes or split votes or secondary votes come up multiple times every release. Rather, it means the different parts are related enough and small enough to be digestible in a single RFC, but each can stand on their own if necessary. In this case, nothing about the get hook necessitates a set hook, and nothing about the set hook necessitates the get hook.
--Larry Garfield
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 properreadonly
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 thereadonly
-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
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.:
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:
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
As Nick has graciously provided an implementation, we would like to
open discussion on this very small RFC to allowreadonly
on backed
properties even if they have a hook defined.
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
Hey all,
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
Le 11 juil. 2025 à 06:30, Nick php@nicksdot.dev a écrit :
Hey all,
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.comTo 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/18757I 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:
-
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;
-
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:
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
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/18757I believe that these are fair solutions to address the concerns that came up in the discussion, and I hope people will agree.
Cheers,
NickHi 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:
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;
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:
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
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 forreadonly
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 mandatorycached
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 thereadonly
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
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
computedget
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/18757I believe that these are fair solutions to address the concerns that
came up in the discussion, and I hope people will agree.Cheers,
NickHi 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:
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;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:
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 forreadonly
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 mandatorycached
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 thereadonly
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 nonreadonly
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
Cheers,
Nick
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
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 changedThe 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 versionDo 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 acached
modifier voids this for no strong reason (please
see “mental model” above).
See above - and init
is just once more character.
--
Cheers,
Nick[1] https://news-web.php.net/php.internals/128010
[2] https://news-web.php.net/php.internals/128007
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 writeinit =>
random_int();
instead ofcached 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".
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 writeinit =>
random_int();
instead ofcached 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).
// 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
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).
// 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.
Hi Nick
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
Hi Nick
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/18757I 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 callingserialize()
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
Le lun. 14 juil. 2025 à 15:41, Larry Garfield larry@garfieldtech.com a
écrit :
Hi Nick
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/18757I 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 callingserialize()
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
Le lun. 14 juil. 2025 à 15:41, Larry Garfield larry@garfieldtech.com a écrit :
Hi Nick
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/18757I 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 callingserialize()
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
Le lun. 14 juil. 2025 à 15:41, Larry Garfield larry@garfieldtech.com a écrit :
Hi Nick
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/18757I 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 callingserialize()
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
.
Le lun. 14 juil. 2025 à 15:41, Larry Garfield larry@garfieldtech.com a écrit :
Hi Nick
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/18757I 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 callingserialize()
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
andget
hooks for validation? I would think onlyset
hooks would be
necessary, and I don't yet think I've seen an objection toset
hooks
forreadonly
.
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
Hi
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
Hi
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 viavar_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
Hi
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 viavar_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.
Hi
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 viavar_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 aninit
hook, we'd have solved this problem, and could deprecate the__get
hack forreadonly
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 forreadonly
properties once we had aninit
hook - I'm not proposing deprecating
it generally. Without a counterexample, I don't think there would be
another reason for__get
to work withreadonly
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
Hi
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 viavar_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 aninit
hook, we'd have solved this problem, and could deprecate the__get
hack forreadonly
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 forreadonly
properties once we had aninit
hook - I'm not proposing deprecating
it generally. Without a counterexample, I don't think there would be
another reason for__get
to work withreadonly
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
Hi
[dropping most of the folks from the Cc list to reduce noise a little]
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 onset
(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
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
Hi
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
As Nick has graciously provided an implementation, we would like to
open discussion on this very small RFC to allowreadonly
on backed
properties even if they have a hook defined.
"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
Hi
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:
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.
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.
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.
// 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)
.
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.
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.
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.
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
Le ven. 18 juil. 2025 à 18:32, Tim Düsterhus tim@bastelstu.be a écrit :
Hi
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:
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...
The concern would only appear if someone is deliberately doing something
non-stableOr if someone accidentally calls a (deep) function chain that is non-pure.
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.
// 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 thatreadonly
impliesprotected(set)
.
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 apublic(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.
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.
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.
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
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
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:
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.
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.
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.
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.
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
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:
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 passinit
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 aninit
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 withget
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 needget
hook capabilities and not in factinit
hook capabilities. If they
needed genericget
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
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
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
Le 19 juil. 2025 à 09:46, Rob Landers rob@bottled.codes a écrit :
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
Le 19 juil. 2025 à 09:46, Rob Landers rob@bottled.codes a écrit :
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
Hi Rob,
I'm going to respond to a few points from earlier emails here instead
of each one.
Le 19 juil. 2025 à 09:46, Rob Landers rob@bottled.codes a écrit :
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.)
I do not believe I was cherry picking; I share Claude's interpretation
here that the RFC says that readonly doesn't prevent the language from
adopting accessors (hooks) later, which is what the language did, and
notably it did without applying them to readonly properties.
Could you perhaps walk me through your thinking when the RFC claims
that assert($prop === $prop2)
"always holds"?
—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.
(disclaimer, I am not a C# expert)
It seems that C# has both fields and properties, and a readonly field
seems to align with what a few of us are claiming is how PHP readonly
properties should work. C# properties are more open-ended, and don't
actually support the readonly keyword - you can make them "read only"
in the sense of only having a get accessor (and you can mark that
accessor itself as readonly), but this is different from readonly
fields, which enforce a contract about the mutability of the field.
I think that C# fields, both readonly and not, match PHP's properties
without hooks. C# properties - which I believe cannot be marked
readonly, but they can be made read only, i.e. only exposing a get
accessor - match PHP's properties with hooks.
Hi Rob,
The main problem is that we don’t agree on the meaning of “readonly property”.
I agree with Claude here. Rob, I'm curious how you interpret the
contract of readonly. Do you think that it is a contract about
writability to consumers of the class? In an earlier email, you said:
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.
In this most recent email, you said:
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.
Would you say this is how you are interpreting readonly? That it is a
contract to consumers of the class that they can read but cannot write
to it?
(including a snippet from an earlier email)
I think an init hook is out of the question for 8.5, so I'm not even sure it's worth discussing.
I don't agree that it's not worth discussing alternative solutions to
the problems the RFC authors intend to solve. I think it has been
expressed elsewhere, but I believe we should take the time to make the
best decisions for the language and not rush to add a controversial
feature.
Hi Rob,
I'm going to respond to a few points from earlier emails here instead
of each one.Le 19 juil. 2025 à 09:46, Rob Landers rob@bottled.codes a écrit :
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.)
I do not believe I was cherry picking; I share Claude's interpretation
here that the RFC says that readonly doesn't prevent the language from
adopting accessors (hooks) later, which is what the language did, and
notably it did without applying them to readonly properties.Could you perhaps walk me through your thinking when the RFC claims
thatassert($prop === $prop2)
"always holds"?
Hi Eric,
I think that is explaining how Nikita arrived at some of the conclusions (it is in the rationale section, after all) and shouldn't be taken as literal. Here are some snippets that I think should be looked at a little more closely, that seem to align with the vision of the feature, in the context of getters/setters:
"The closest alternative is to declare the property private, and only expose a public getter" -- indicates to me that a single public getter should, in fact, be considered readonly.
"Support for first-class readonly properties allows you to directly expose public readonly properties, without fear that class invariants could be broken through external modification" -- indicates to me that interior mutability (and maybe nondeterminism) should be at the discretion of the interior, not the exterior contract.
"...readonly properties do not preclude interior mutability. Objects (or resources) stored in readonly properties may still be modified internally" -- further specifies that interior mutability is allowed; only exterior mutability isn't.
It doesn't define "interior mutability" stringently, so we can have differing opinions on that; but it seems to be "the value inside the property" which may or may not be an object, resource, or a hook's result.
—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.
(disclaimer, I am not a C# expert)
It seems that C# has both fields and properties, and a readonly field
seems to align with what a few of us are claiming is how PHP readonly
properties should work. C# properties are more open-ended, and don't
actually support the readonly keyword - you can make them "read only"
in the sense of only having a get accessor (and you can mark that
accessor itself as readonly), but this is different from readonly
fields, which enforce a contract about the mutability of the field.I think that C# fields, both readonly and not, match PHP's properties
without hooks. C# properties - which I believe cannot be marked
readonly, but they can be made read only, i.e. only exposing a get
accessor - match PHP's properties with hooks.
I would prefer to simply allow specifying a class as readonly so long as only get hooks are present. However, I'm ok with saying that get hooks may themselves be readonly which accomplishes the same thing and is easier to reason about.
Hi Rob,
The main problem is that we don’t agree on the meaning of “readonly property”.
I agree with Claude here. Rob, I'm curious how you interpret the
contract of readonly. Do you think that it is a contract about
writability to consumers of the class? In an earlier email, you said: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.
In this most recent email, you said:
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.
Would you say this is how you are interpreting readonly? That it is a
contract to consumers of the class that they can read but cannot write
to it?
That's the definition in the RFC, from my reading of it: the ability to expose bare properties without worrying that someone else will modify it. One might argue that there really isn't a reason for readonly anymore, since we have aviz + hooks. Or rather, that readonly could be just a shorthand for that -- plus the ability to write to those properties exactly-once. I suspect this is where "write-once" is coming from elsewhere in the thread, since that is the only difference between "manual readonly" and "engine-powered readonly".
(including a snippet from an earlier email)
I think an init hook is out of the question for 8.5, so I'm not even sure it's worth discussing.
I don't agree that it's not worth discussing alternative solutions to
the problems the RFC authors intend to solve. I think it has been
expressed elsewhere, but I believe we should take the time to make the
best decisions for the language and not rush to add a controversial
feature.
Would an init hook actually solve it though? An init hook would basically just be a constructor / default value outside of the constructor that specifies an instance-level constant. The readonly property RFC goes into some details here:
"As the default value counts as an initializing assignment, a readonly property with a default value is essentially the same as a constant, and thus not particularly useful. The notion could become more useful in the future, if new expressions are allowed as property default values. At the same time, depending on how exactly property initialization would work in that case, having a default value on a readonly property could preclude userland serialization libraries from working, as they would not be able to replace the default-constructed object. Whether or not this is a concern depends on whether the property is initialized at time of object creation, or as an implicit part of the constructor (or similar). As these are open questions, the conservative choice is to forbid default values until these questions are resolved."
When would the init hook get called? And in what order? This brings back some memories of solving some Java language bugs where static variables wouldn't be initialized in time (they're non-deterministic) causing strange crashes, or the potential to "deadlock" yourself and you need to read properties in a specific order in order to ensure the object gets initialized correctly.
This isn't something that can be solved in a few weeks. Someone(s) needs to sit down and think through all the possibilities and then create a definition of an init hook that is better than a constructor.
— Rob
Hi Rob,
I'm going to respond to a few points from earlier emails here instead
of each one.Le 19 juil. 2025 à 09:46, Rob Landers rob@bottled.codes a écrit :
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.)
I do not believe I was cherry picking; I share Claude's interpretation
here that the RFC says that readonly doesn't prevent the language from
adopting accessors (hooks) later, which is what the language did, and
notably it did without applying them to readonly properties.Could you perhaps walk me through your thinking when the RFC claims
thatassert($prop === $prop2)
"always holds"?Hi Eric,
I think that is explaining how Nikita arrived at some of the conclusions (it is in the rationale section, after all) and shouldn't be taken as literal. Here are some snippets that I think should be looked at a little more closely, that seem to align with the vision of the feature, in the context of getters/setters:
"The closest alternative is to declare the property private, and only expose a public getter" -- indicates to me that a single public getter should, in fact, be considered readonly.
The very next line says, "This doesn't actually make the property
readonly, but it does tighten the scope where modification could occur
to a single class declaration." I interpret this as "the property can
still be written to internally many times, but at least you don't
have to look very far to confirm that this doesn't happen".
Or to put it another way, the RFC is claiming that this makes it so
that a consumer cannot mutate the value freely, only the class can,
but this is not in fact readonly.
"Support for first-class readonly properties allows you to directly expose public readonly properties, without fear that class invariants could be broken through external modification" -- indicates to me that interior mutability (and maybe nondeterminism) should be at the discretion of the interior, not the exterior contract.
I think this is building on top of the above, indicating that readonly
allows it to be clear that no one can write to this property multiple
times, even though it is publicly exposed.
"...readonly properties do not preclude interior mutability. Objects (or resources) stored in readonly properties may still be modified internally" -- further specifies that interior mutability is allowed; only exterior mutability isn't.
It doesn't define "interior mutability" stringently, so we can have differing opinions on that; but it seems to be "the value inside the property" which may or may not be an object, resource, or a hook's result.
I do have a differing opinion, and will quote the first line of the
RFC - "This RFC introduces a readonly property modifier, which
prevents modification of the property after initialization." It seems
pretty clear to me that this is talking about ensuring that a property
is initialized once and the property itself will not change, but if
the property is an object, the object's properties are free to change.
I agree that the readonly keyword itself leaves room for
interpretation as I mentioned in my earlier email, but I just can't
seem to read this RFC as anything other than promising immutability,
especially considering this first line.
—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.
(disclaimer, I am not a C# expert)
It seems that C# has both fields and properties, and a readonly field
seems to align with what a few of us are claiming is how PHP readonly
properties should work. C# properties are more open-ended, and don't
actually support the readonly keyword - you can make them "read only"
in the sense of only having a get accessor (and you can mark that
accessor itself as readonly), but this is different from readonly
fields, which enforce a contract about the mutability of the field.I think that C# fields, both readonly and not, match PHP's properties
without hooks. C# properties - which I believe cannot be marked
readonly, but they can be made read only, i.e. only exposing a get
accessor - match PHP's properties with hooks.I would prefer to simply allow specifying a class as readonly so long as only get hooks are present. However, I'm ok with saying that get hooks may themselves be readonly which accomplishes the same thing and is easier to reason about.
I'm sorry, I don't follow the second sentence. My main point was that
C#'s readonly fields cannot have accessors, which I believe are 1:1,
invariant-wise, with PHP's readonly properties. C#'s properties are
1:1 with PHP's properties with hooks.
C# does have readonly accessors for properties, but this is a separate
invariant that makes the compiler ensure that the get acessor itself
cannot modify the state of the object, and is separate from the notion
of a readonly field.
Hi Rob,
The main problem is that we don’t agree on the meaning of “readonly property”.
I agree with Claude here. Rob, I'm curious how you interpret the
contract of readonly. Do you think that it is a contract about
writability to consumers of the class? In an earlier email, you said: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.
In this most recent email, you said:
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.
Would you say this is how you are interpreting readonly? That it is a
contract to consumers of the class that they can read but cannot write
to it?That's the definition in the RFC, from my reading of it: the ability to expose bare properties without worrying that someone else will modify it. One might argue that there really isn't a reason for readonly anymore, since we have aviz + hooks. Or rather, that readonly could be just a shorthand for that -- plus the ability to write to those properties exactly-once. I suspect this is where "write-once" is coming from elsewhere in the thread, since that is the only difference between "manual readonly" and "engine-powered readonly".
I would not argue that there isn't a reason for readonly anymore due
to aviz, I would argue that readonly means something different from
aviz, which is exactly how I read the RFC.
Can I ask what you would like to use the readonly keyword for? I would
like to use it to, as the RFC states, "[prevent] modification of the
property after initialization". Would you also like to use it to
prevent modification of the property after initialization, or would
you like to use it to signify that it is public protected(set)
? The
latter, to me, is an implementation detail of readonly and not the
main point; if you want to declare asymmetric visibility, use
asymmetric visibility.
(including a snippet from an earlier email)
I think an init hook is out of the question for 8.5, so I'm not even sure it's worth discussing.
I don't agree that it's not worth discussing alternative solutions to
the problems the RFC authors intend to solve. I think it has been
expressed elsewhere, but I believe we should take the time to make the
best decisions for the language and not rush to add a controversial
feature.Would an init hook actually solve it though? An init hook would basically just be a constructor / default value outside of the constructor that specifies an instance-level constant. The readonly property RFC goes into some details here:
"As the default value counts as an initializing assignment, a readonly property with a default value is essentially the same as a constant, and thus not particularly useful. The notion could become more useful in the future, if new expressions are allowed as property default values. At the same time, depending on how exactly property initialization would work in that case, having a default value on a readonly property could preclude userland serialization libraries from working, as they would not be able to replace the default-constructed object. Whether or not this is a concern depends on whether the property is initialized at time of object creation, or as an implicit part of the constructor (or similar). As these are open questions, the conservative choice is to forbid default values until these questions are resolved."
An init hook would not be a default value outside of the constructor
that specifies an instance level constant, what gives you that
impression? The problem that people have pointed to is lazy
initialization, e.g. fetching something from the database when
accessed. This very RFC mentions lazy initialization as the use-case
for get hooks here:
https://wiki.php.net/rfc/readonly_hooks#orms_and_proxies.
I believe that an init hook would solve this problem instead, and
maintain the invariant of immutability for readonly properties.
When would the init hook get called? And in what order? This brings back some memories of solving some Java language bugs where static variables wouldn't be initialized in time (they're non-deterministic) causing strange crashes, or the potential to "deadlock" yourself and you need to read properties in a specific order in order to ensure the object gets initialized correctly.
Tim and I separately offered answers to these questions earlier; in
any case I believe it's a tractable problem if our initial suggestions
aren't enough.
This isn't something that can be solved in a few weeks. Someone(s) needs to sit down and think through all the possibilities and then create a definition of an init hook that is better than a constructor.
I agree this might not be solved in a few weeks, but since I don't
think we need to rush this before 8.5, I don't see this as a problem.
Hi Rob,
I'm going to respond to a few points from earlier emails here instead
of each one.Le 19 juil. 2025 à 09:46, Rob Landers rob@bottled.codes a écrit :
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.)
I do not believe I was cherry picking; I share Claude's interpretation
here that the RFC says that readonly doesn't prevent the language from
adopting accessors (hooks) later, which is what the language did, and
notably it did without applying them to readonly properties.Could you perhaps walk me through your thinking when the RFC claims
thatassert($prop === $prop2)
"always holds"?Hi Eric,
I think that is explaining how Nikita arrived at some of the conclusions (it is in the rationale section, after all) and shouldn't be taken as literal. Here are some snippets that I think should be looked at a little more closely, that seem to align with the vision of the feature, in the context of getters/setters:
"The closest alternative is to declare the property private, and only expose a public getter" -- indicates to me that a single public getter should, in fact, be considered readonly.
The very next line says, "This doesn't actually make the property
readonly, but it does tighten the scope where modification could occur
to a single class declaration." I interpret this as "the property can
still be written to internally many times, but at least you don't
have to look very far to confirm that this doesn't happen".
I think the intent was to show how public readonly could be implemented at that time. However, if you want private readonly, you cannot have that in the version of PHP at that time.
Or to put it another way, the RFC is claiming that this makes it so
that a consumer cannot mutate the value freely, only the class can,
but this is not in fact readonly.
It takes the position that the class itself can be the consumer and provides a way to express it through write-once properties.
"Support for first-class readonly properties allows you to directly expose public readonly properties, without fear that class invariants could be broken through external modification" -- indicates to me that interior mutability (and maybe nondeterminism) should be at the discretion of the interior, not the exterior contract.
I think this is building on top of the above, indicating that readonly
allows it to be clear that no one can write to this property multiple
times, even though it is publicly exposed.
It doesn't say "no one" it only says "class invariants" which may mean different things in different designs and systems. If your design specifies that a value is invariant, that could mean it is "always constant", or on the other extreme, could mean it is "always random". When the design aligns with readonly, then you can have the engine enforce it.
"...readonly properties do not preclude interior mutability. Objects (or resources) stored in readonly properties may still be modified internally" -- further specifies that interior mutability is allowed; only exterior mutability isn't.
It doesn't define "interior mutability" stringently, so we can have differing opinions on that; but it seems to be "the value inside the property" which may or may not be an object, resource, or a hook's result.
I do have a differing opinion, and will quote the first line of the
RFC - "This RFC introduces a readonly property modifier, which
prevents modification of the property after initialization." It seems
pretty clear to me that this is talking about ensuring that a property
is initialized once and the property itself will not change, but if
the property is an object, the object's properties are free to change.I agree that the readonly keyword itself leaves room for
interpretation as I mentioned in my earlier email, but I just can't
seem to read this RFC as anything other than promising immutability,
especially considering this first line.
You can also implement this today, without using readonly:
class Foo {
public int $bar {
get => $this->bar;
set => empty($this->bar) ? $this->bar = $value : throw new LogicException("nope");
}
}
https://3v4l.org/2JagR#v8.4.10
Should I be able to mark this class as readonly? I would think so. That's the question this new RFC is proposing, in my mind. It is saying that hooks are allowed to be readonly, allowing me to specify my own invariants, to a degree, and having the engine enforce them where it can while also signifying consumers there are some obvious invariants. Sure, you could do other shenanigans in the get-hooks, but it is a programming language -- there will always be shenanigans.
—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.
(disclaimer, I am not a C# expert)
It seems that C# has both fields and properties, and a readonly field
seems to align with what a few of us are claiming is how PHP readonly
properties should work. C# properties are more open-ended, and don't
actually support the readonly keyword - you can make them "read only"
in the sense of only having a get accessor (and you can mark that
accessor itself as readonly), but this is different from readonly
fields, which enforce a contract about the mutability of the field.I think that C# fields, both readonly and not, match PHP's properties
without hooks. C# properties - which I believe cannot be marked
readonly, but they can be made read only, i.e. only exposing a get
accessor - match PHP's properties with hooks.I would prefer to simply allow specifying a class as readonly so long as only get hooks are present. However, I'm ok with saying that get hooks may themselves be readonly which accomplishes the same thing and is easier to reason about.
I'm sorry, I don't follow the second sentence. My main point was that
C#'s readonly fields cannot have accessors, which I believe are 1:1,
invariant-wise, with PHP's readonly properties. C#'s properties are
1:1 with PHP's properties with hooks.C# does have readonly accessors for properties, but this is a separate
invariant that makes the compiler ensure that the get acessor itself
cannot modify the state of the object, and is separate from the notion
of a readonly field.
Correct! Yes, sorry. I didn't explain well. A readonly class in PHP is effectively copy-pasting readonly before every property. This means we either need to redefine what that means, or allow readonly to be defined on hooks so that a class using hooks may be defined readonly in special circumstances.
Hi Rob,
The main problem is that we don’t agree on the meaning of “readonly property”.
I agree with Claude here. Rob, I'm curious how you interpret the
contract of readonly. Do you think that it is a contract about
writability to consumers of the class? In an earlier email, you said: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.
In this most recent email, you said:
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.
Would you say this is how you are interpreting readonly? That it is a
contract to consumers of the class that they can read but cannot write
to it?That's the definition in the RFC, from my reading of it: the ability to expose bare properties without worrying that someone else will modify it. One might argue that there really isn't a reason for readonly anymore, since we have aviz + hooks. Or rather, that readonly could be just a shorthand for that -- plus the ability to write to those properties exactly-once. I suspect this is where "write-once" is coming from elsewhere in the thread, since that is the only difference between "manual readonly" and "engine-powered readonly".
I would not argue that there isn't a reason for readonly anymore due
to aviz, I would argue that readonly means something different from
aviz, which is exactly how I read the RFC.Can I ask what you would like to use the readonly keyword for? I would
like to use it to, as the RFC states, "[prevent] modification of the
property after initialization". Would you also like to use it to
prevent modification of the property after initialization, or would
you like to use it to signify that it ispublic protected(set)
? The
latter, to me, is an implementation detail of readonly and not the
main point; if you want to declare asymmetric visibility, use
asymmetric visibility.
The problem I've run into, especially in 8.3+ is that I like value objects. Ever since reading Domain Driven Design by Eric Evans in 2006-07, I love how simply using them can remove a ton of procedural code, enforce invariants, and make reasoning through code so much simpler (see Chapter 10). The readonly keyword simplified that greatly, however, readonly has been neutered compared to regular classes in the last couple of versions. There are so many edge cases and non-implemented features with them -- mostly due to this exact argument you are making -- that they're nearly worthless to actually define immutable value objects in today's PHP. This is why I ever even began with the Records RFC ... between all the edge cases and weird behaviors, I want to create "actually immutable value objects" that are just as powerful as regular classes and leave readonly for when you need immutable parts of classes.
(including a snippet from an earlier email)
I think an init hook is out of the question for 8.5, so I'm not even sure it's worth discussing.
I don't agree that it's not worth discussing alternative solutions to
the problems the RFC authors intend to solve. I think it has been
expressed elsewhere, but I believe we should take the time to make the
best decisions for the language and not rush to add a controversial
feature.Would an init hook actually solve it though? An init hook would basically just be a constructor / default value outside of the constructor that specifies an instance-level constant. The readonly property RFC goes into some details here:
"As the default value counts as an initializing assignment, a readonly property with a default value is essentially the same as a constant, and thus not particularly useful. The notion could become more useful in the future, if new expressions are allowed as property default values. At the same time, depending on how exactly property initialization would work in that case, having a default value on a readonly property could preclude userland serialization libraries from working, as they would not be able to replace the default-constructed object. Whether or not this is a concern depends on whether the property is initialized at time of object creation, or as an implicit part of the constructor (or similar). As these are open questions, the conservative choice is to forbid default values until these questions are resolved."
An init hook would not be a default value outside of the constructor
that specifies an instance level constant, what gives you that
impression? The problem that people have pointed to is lazy
initialization, e.g. fetching something from the database when
accessed. This very RFC mentions lazy initialization as the use-case
for get hooks here:
https://wiki.php.net/rfc/readonly_hooks#orms_and_proxies.I believe that an init hook would solve this problem instead, and
maintain the invariant of immutability for readonly properties.When would the init hook get called? And in what order? This brings back some memories of solving some Java language bugs where static variables wouldn't be initialized in time (they're non-deterministic) causing strange crashes, or the potential to "deadlock" yourself and you need to read properties in a specific order in order to ensure the object gets initialized correctly.
Tim and I separately offered answers to these questions earlier; in
any case I believe it's a tractable problem if our initial suggestions
aren't enough.This isn't something that can be solved in a few weeks. Someone(s) needs to sit down and think through all the possibilities and then create a definition of an init hook that is better than a constructor.
I agree this might not be solved in a few weeks, but since I don't
think we need to rush this before 8.5, I don't see this as a problem.
I honestly haven't read the arguments for init-hooks. Mostly because I saw that part of the conversation and it reminds me of when other people start proposing their ideas in other people's RFCs. Sometimes they're good ideas, sometimes their not, but I leave that to the author to digest and make changes to the RFC. I don't agree with init-hooks for the reasons I gave, but until the RFC author incorporates it into the RFC, I tend to ignore those types of things. The author is the subject-matter expert and they're the ones that know if something is a good idea or not. Engaging in those parts of the discussions just leads to off-topic discussions.
— Rob
Le 22 juil. 2025 à 09:43, Rob Landers rob@bottled.codes a écrit :
You can also implement this today, without using readonly:
class Foo {
public int $bar {
get => $this->bar;
set => empty($this->bar) ? $this->bar = $value : throw new LogicException("nope");
}
}
Your code is buggy, because 0
is “empty”:
https://3v4l.org/iUPvW#v8.4.10
Should I be able to mark this class as readonly? I would think so.
I don’t think so.
If you want to document the intended invariant, you can put a @readonly tag in a phpdoc comment.
Adding a readonly
keyword should enforce the invariant; the added value is that it would choke on bugs like the one you wrote just above, making it debugging much easier.
The readonly keyword simplified that greatly, however, readonly has been neutered compared to regular classes in the last couple of versions. There are so many edge cases and non-implemented features with them -- mostly due to this exact argument you are making -- that they're nearly worthless to actually define immutable value objects in today's PHP.
There are several issues with the readonly feature, mostly because of the “worse-is-better” philosophy. But we can slowly correct the main issues instead of making them worse.
—Claude
Le 22 juil. 2025 à 09:43, Rob Landers rob@bottled.codes a écrit :
You can also implement this today, without using readonly:
class Foo {
public int $bar {
get => $this->bar;
set => empty($this->bar) ? $this->bar = $value : throw new LogicException("nope");
}
}Your code is buggy, because
0
is “empty”:
I wrote it over 30s while getting ready to walk out the door for a week long vacation, so I won’t be entirely surprised if it is wrong. :)
Should I be able to mark this class as readonly? I would think so.
I don’t think so.
If you want to document the intended invariant, you can put a @readonly tag in a phpdoc comment.
Adding a
readonly
keyword should enforce the invariant; the added value is that it would choke on bugs like the one you wrote just above, making it debugging much easier.
I’m not sure if you meant to, but I feel like you just argued for allowing readonly on hooks so that these kinds of bugs aren’t accidentally written…
The readonly keyword simplified that greatly, however, readonly has been neutered compared to regular classes in the last couple of versions. There are so many edge cases and non-implemented features with them -- mostly due to this exact argument you are making -- that they're nearly worthless to actually define immutable value objects in today's PHP.
There are several issues with the readonly feature, mostly because of the “worse-is-better” philosophy. But we can slowly correct the main issues instead of making them worse.
—Claude
I think this is the main crux of the issue, right? There is one camp that says readonly means immutable and another that says readonly is read-only. These two viewpoints are not compatible despite having a lot of overlap.
— Rob
Le 22 juil. 2025 à 11:25, Rob Landers rob@bottled.codes a écrit :
Should I be able to mark this class as readonly? I would think so.
I don’t think so.
If you want to document the intended invariant, you can put a @readonly tag in a phpdoc comment.
Adding a
readonly
keyword should enforce the invariant; the added value is that it would choke on bugs like the one you wrote just above, making it debugging much easier.I’m not sure if you meant to, but I feel like you just argued for allowing readonly on hooks so that these kinds of bugs aren’t accidentally written…
You are misinterpreting my statement, and I’m not sure how I could be clearer (additional words will be subject to additional misinterpretation). I didn’t say: “we should allow readonly
somewhere”, but: “if we add readonly
somewhere, it should enforce what it means, not just document an intent”.
The readonly keyword simplified that greatly, however, readonly has been neutered compared to regular classes in the last couple of versions. There are so many edge cases and non-implemented features with them -- mostly due to this exact argument you are making -- that they're nearly worthless to actually define immutable value objects in today's PHP.
There are several issues with the readonly feature, mostly because of the “worse-is-better” philosophy. But we can slowly correct the main issues instead of making them worse.
—Claude
I think this is the main crux of the issue, right? There is one camp that says readonly means immutable and another that says readonly is read-only. These two viewpoints are not compatible despite having a lot of overlap.
The original RFC explicitly and unambiguously gives the intended interpretation, despite your attempts to find another interpretation in it. I agree that the word “readonly” is badly chosen and confusing. But I don’t agree that we should reinterpret any feature based on incorrect understanding of its intent (unless, of course, there is consensus to reinterpret it).
—Claude
Le 22 juil. 2025 à 11:25, Rob Landers rob@bottled.codes a écrit :
Should I be able to mark this class as readonly? I would think so.
I don’t think so.
If you want to document the intended invariant, you can put a @readonly tag in a phpdoc comment.
Adding a
readonly
keyword should enforce the invariant; the added value is that it would choke on bugs like the one you wrote just above, making it debugging much easier.I’m not sure if you meant to, but I feel like you just argued for allowing readonly on hooks so that these kinds of bugs aren’t accidentally written…
You are misinterpreting my statement, and I’m not sure how I could be clearer (additional words will be subject to additional misinterpretation). I didn’t say: “we should allow
readonly
somewhere”, but: “if we addreadonly
somewhere, it should enforce what it means, not just document an intent”.
If words could only be interpreted one way, we wouldn't need lawyers and clergy. I think it is worth clarifying if you are willing. But I do think we agree that it should be enforced -- it just comes down to the what and how it is enforced.
The readonly keyword simplified that greatly, however, readonly has been neutered compared to regular classes in the last couple of versions. There are so many edge cases and non-implemented features with them -- mostly due to this exact argument you are making -- that they're nearly worthless to actually define immutable value objects in today's PHP.
There are several issues with the readonly feature, mostly because of the “worse-is-better” philosophy. But we can slowly correct the main issues instead of making them worse.
—Claude
I think this is the main crux of the issue, right? There is one camp that says readonly means immutable and another that says readonly is read-only. These two viewpoints are not compatible despite having a lot of overlap.
The original RFC explicitly and unambiguously gives the intended interpretation, despite your attempts to find another interpretation in it. I agree that the word “readonly” is badly chosen and confusing. But I don’t agree that we should reinterpret any feature based on incorrect understanding of its intent (unless, of course, there is consensus to reinterpret it).
—Claude
If it were unambiguous, we wouldn't be having this conversation. I'd also invite you to read the original discussion: https://externals.io/message/114729. Note that it is pretty clear that readonly and aviz/hooks overlap, even back then. We do need to discover this overlap, and how it overlaps. Maybe that isn't this RFC, but where they overlap, they should be able to be used together. Or not? I dunno.
— Rob
// 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 thatreadonly
impliesprotected(set)
.
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 apublic(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) ...
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