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.
--
Larry Garfield
larry@garfieldtech.com
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.
--
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
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,
On 8. Jun 2025, at 19:17, Volker Dusch volker@tideways-gmbh.com wrote:
a) From the “ProductFromDB” i get:
Fatal error: Uncaught TypeError: LazyProduct::$category::get(): Return value must be of type Category, none returned in …
I assume you’re missing a return statement here? Or is there something I’m missing?
d) PositivePoint
Example doesn’t compile against 8.4, master, or against NickSdot:readonly-hooks
Can you make this a script that runs and shows expected output so that readers don’t have to assume this is supposed to do or run it?
Unfortunate typos in the RFC text. I can’t yet update the RFC text myself, but we will make sure they will be fixed shortly!
Meanwhile, you can find the full running code for both examples here:
NickSdot:readonly-hooks/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt
NickSdot:readonly-hooks/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt
Thanks for pointing these out!
—
I leave addressing the other points to Larry.
Cheers,
Nick
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
readonlyon backed properties even if they have a hook defined.–
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
On Sun, Jun 8, 2025, at 7:17 AM, Volker Dusch wrote:
Hey Larry,
Couple points from a first read and from trying to run the examples.
a) From the "ProductFromDB" i get:
Fatal error: Uncaught TypeError: LazyProduct::$category::get(): Return value must be of type Category, none returned in ...
I assume you're missing a return statement here? Or is there something
I'm missing?b) Minor wording gripe in the proposal section
On the other hand, there is no shortage of dumb things that people can do with PHP already.
While I don't disagree that PHP gives people a lot of freedom in how
they want to write their code, I find it a bit crude for an RFC to
phrase it like that. And the __get comparison is strong enough to stand
on its own.
Various typos have been fixed, and the code should be valid now. The wording has also been adapted (though I am pretty sure "silly" has appeared in RFCs before).
I also fleshed out the __get mention with an example that shows what you can already do today, and in fact could since 8.1 when readonly was introduced. The hard guarantee of idempotency has never actually been there. (This also speaks to Claude's concern.)
c)
That is, we feel, an entirely reasonable use of hooks, and would allow for lazy-load behavior per-property on readonly classes.
I might be misunderstanding the sentence here, but on-demand/Lazy
initialization of properties on readonly classes is already possible in
classic getter/setter classes.
If a class uses private properties and getX/setX methods, sure, those methods can be overridden to do whatever you want. The whole point of hooks, though, is to NOT need those methods. We want to enable someone to define a read-model easily, like the Product in the example. That's all they should need to specify. So, yes, technically it's "in a class that is designed with modern features" rather than having a long list of getX/setX methods just to give lazy-loading generated code a place to hook in.
f) Date: 2024-07-10
Is this correct? I know you created the page back then, but was there a
discussion already that I wasn't able to find?
Yes, this RFC was originally spun off from the hook-improvements RFC, as it needed more discussion while the other half of that RFC was uncontroversial. I've added a link to the prior thread for reference.
--Larry Garfield
On Sat, Jun 7, 2025, at 11:16 PM, Larry Garfield wrote:
As Nick has graciously provided an implementation, we would like to
open discussion on this very small RFC to allow `readonly` on backed
properties even if they have a hook defined.
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
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
On Tue, Jul 1, 2025, at 9:27 AM, Tim Düsterhus wrote:
Hi
Am 2025-06-09 17:11, schrieb Larry Garfield:
I also fleshed out the __get mention with an example that shows what
you can already do today, and in fact could since 8.1 when readonly was
introduced. The hard guarantee of idempotency has never actually been
there. (This also speaks to Claude's concern.)I don't think this really resolves Claude's concern. While it is
certainly true that the guarantees do not currently hold, I don't
believe this is strong enough of a reason not to provide for stronger
guarantees in a *newly introduced feature*. The point of property hooks
is for me that “dynamic properties” are easier to reason about compared
to `__get()`. As a user when accessing a proper `readonly` property, I
do not want to check if there is a property hook that might result in
non-readonly behavior. As an engine developer I want to be able to
optimize based on the `readonly`-ness of a property. Without such
guarantees, the “readonly” keyword does not provide value to me.
The only way to make the readonliness fully guaranteed would be to force a readonly property to be cached; (IE, the hook is only called at all if the property is uninitialized.) But there's no obvious way to make that clear in the code that it's what's happening.
I also believe the LazyProduct example to be broken, since lazy-loading
individual properties might result in an object that is internally
consistent if the database changes in-between.
That's true with any lazy-loading scenario. The use of hooks doesn't change that at all.
--Larry Garfield
Hi Larry et al.
Le mar. 8 juil. 2025 à 17:12, Larry Garfield <larry@garfieldtech.com> a écrit :
On Tue, Jul 1, 2025, at 9:27 AM, Tim Düsterhus wrote:
Hi
Am 2025-06-09 17:11, schrieb Larry Garfield:
I also fleshed out the __get mention with an example that shows what
you can already do today, and in fact could since 8.1 when readonly was
introduced. The hard guarantee of idempotency has never actually been
there. (This also speaks to Claude’s concern.)I don’t think this really resolves Claude’s concern. While it is
certainly true that the guarantees do not currently hold, I don’t
believe this is strong enough of a reason not to provide for stronger
guarantees in a newly introduced feature. The point of property hooks
is for me that “dynamic properties” are easier to reason about compared
to__get(). As a user when accessing a properreadonlyproperty, 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.
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 properreadonlyproperty, 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:
Nicolas
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 an
inithook).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
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
inithook).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)
On Wed, Jul 9, 2025, at 5:52 AM, Claude Pache wrote:
Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit :
I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient.
It is true that readonly could be bypassed by __get(); but this is a
legacy behaviour, and you have to take an explicit step to make it
possible. For those unaware of the awful hack, here is a minimal test
case:Online PHP editor | output for N78An
where the `unset(...)` is mandatory to make it “work”.
Are we obligated to sanction shortcomings of legacy concepts in newly
introduced concepts that are supposed to replace them? Or can we do
something better? I’ve outlined in a previous email what I think is a
better design for such situation (namely an `init` hook).Also, the fact that __get() is not yet deprecated means that we can
still use the aforementioned hack until/unless we’ve implemented a
proper solution. In the worst case, you can still use a non-readonly
hooked property and document the intended invariants in phpdoc.If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent.
Enforcing as strictly as possible its intended invariants is a good
design for a robust language. Yes, it implies that users cannot (or can
hardly) escape annoying constraints. For example, you can’t extend a
final class, even if you think that you have good reason for it, like
constructing a mock object.—Claude
Here's the core problem right now:
1. `readonly` bills itself as immutability, but it fundamentally is not. There are at least two loopholes: __get and a mutable object saved to a property. So while it offering immutability guarantees is nice in theory, it's simply not true in practice. `readonly` has always been misnamed; it should really be `writeonce`, because that's all it is. (Once again, this is likely the most poorly designed feature we've added in many years.)
2. In 8.4, if a class is marked `readonly`, you basically forbid it from having any hooks of any kind, even though you absolutely can honor the write-once-ness of the properties while still having hooks. And that applies to child classes, too, because `readonly`-ness inherits. So adding a single hook means you have to move the readonly to all the other properties individually, which if inheritance is involved you cannot do.
The RFC aims to address point 2 in a way that still respects point 1, but only point 1 as it actually is (write-once), not as we wish it to be (immutability).
In practice, there's 2 scenarios that I see as useful (or problematic in 8.4, that we want to support):
* set hooks for validation, which don't impact writeonce-ness. I think everyone seems on board with that.
* Lazy computed properties. I use these a ton, even for internal caching purposes. 99% of the time I cache them because my objects are practically immutable, and $this->foo ??= whatever is an easy enough pattern. (If they're not cached then it would be a virtual property, which we're not dealing with for now.) As long as you're caching it in that fashion, the write-once-ness still ends up respected.
Honestly, Nick tried to come up with examples yesterday while we were talking that would not fit into one of those two categories, and for every one of them my answer was "if your code is already that badly designed, there's nothing we can do for you." ![]()
Ilija and I had discussed making `readonly` imply cached/lazy/init in the original hooks RFC, but decided against it. Mainly, it becomes very confusing if a property is going to store a value, as there's three different scenarios to consider: There's a short-set hook, the property is mentioned in its own hooks, and then look for readonly. (Would that mean readonly only works on virtual properties?) It makes a feature that's already, in all honesty, at the edge of reasonable complexity more complex.
An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???"
I'd be open to a follow up RFC for an init hook, though I likely wouldn't write it myself. But that's a different topic than what we're addressing here.
--Larry Garfield
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.
On Wed, Jul 9, 2025, at 13:39, Nicolas Grekas wrote:
Thanks for your detailed thoughts, Claude. I’d like to offer my perspective on some of the points you raised.
Le mer. 9 juil. 2025 à 12:53, Claude Pache <claude.pache@gmail.com> a écrit :
Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> 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
inithook).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
On Wed, Jul 9, 2025, at 10:42 AM, Eric Norris wrote:
An init hook would be clearer, certainly, though it also has its own edge cases. Can you set something that has an init hook? What happens if there's both a get and init hook? These probably have answers that could be sorted out, but that's a different question from "why the <censored> does a readonly class forbid me using even rudimentary hooks???"
I'd be open to a follow up RFC for an init hook, though I likely wouldn't write it myself. But that's a different topic than what we're addressing here.
--Larry Garfield
I'm not entirely sure I follow - it sounds like your email states that
`readonly` should be interpreted as `writeonce`, which makes sense,
but then why would an `init` hook not be the appropriate answer here?The two scenarios you listed (`set` hooks for validation and lazy
computed properties) seem like they could be solved by allowing `set`
hooks (everyone seems +1 to that), an `init` hook, and disallowing
`get` hooks. It would sidestep the controversial nature of a `get`
hook for the property.It feels to me like an init hook would be the more conservative
approach, and would (I imagine) still allow for potential `readonly`
engine optimizations like Tim pointed out. Once we allow `get` hooks,
there's no going back. If we still needed to add `get` hooks in the
future, it's not off the table.I don't know that I feel strongly here, but there does seem something
intuitively off with allowing a get hook for a readonly (writeonce)
property.
Can an init hook reference itself, the way get and set can?
If there is both an init and set hook, what happens? Is it different if set reads from itself than if it writes to itself?
Should combining init and set be forbidden as confusing?
Can you have both an init hook and a get hook? What happens then?
Repeat all of the above on readonly properties.
I don't know the answer to any of those. We could probably collectively figure out some answers to that in time, but that's a much larger lift than either Nick or I have any interest in engaging in at this point, especially when there is a reasonable solution right in front of us that is trivial to implement.
--Larry Garfield
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
readonlyclass?
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 separateinithook 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
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: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