# [RFC] Readonly property hooks

- Thread: https://externals.io/message/127621
- Original archive: https://news-web.php.net/php.internals/127621
- Started by Larry Garfield on 2025-06-08T04:16:42+00:00
- Messages: 92
- Page 1 of 4: messages 1 to 25 (25 per page, add `?page=N` to the URL)
- Next page: https://externals.io/message/127621.md?page=2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #127621 — Larry Garfield — 2025-06-08T04:16:42+00:00

URL: https://externals.io/message/127621#127621

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

---

## #127622 — Volker Dusch — 2025-06-08T12:17:43+00:00

URL: https://externals.io/message/127621#127622 · In reply to: #127621

Hey Larry,

Couple points from a first read and from trying to run the examples.

a) From the "ProductFromDB" i get:

> Fatal error: Uncaught TypeError: LazyProduct::$category::get(): Return
value must be of type Category, none returned in ...

I assume you're missing a return statement here? Or is there something I'm
missing?

b) Minor wording gripe in the proposal section

> On the other hand, there is no shortage of dumb things that people can do
with PHP already.

While I don't disagree that PHP gives people a lot of freedom in how they
want to write their code, I find it a bit crude for an RFC to phrase it
like that. And the __get comparison is strong enough to stand on its own.

c)

> That is, we feel, an entirely reasonable use of hooks, and would allow
for lazy-load behavior per-property on readonly classes.

I might be misunderstanding the sentence here, but on-demand/Lazy
initialization of properties on readonly classes is already possible in
classic getter/setter classes.

Do you mean you want to have parity to this behavior when using hooks? I'm
all for it, I just feel the sentence says this enables something that
wasn't possible before but if you mean this in scope property access it
makes sense to me.

d) PositivePoint

Example doesn't compile against 8.4, master, or
against NickSdot:readonly-hooks

Can you make this a script that runs and shows expected output so that
readers don't have to assume this is supposed to do or run it?

e) Backward Incompatible Changes

Section is not filled in yet

f) Date: 2024-07-10

Is this correct? I know you created the page back then, but was there a
discussion already that I wasn't able to find?

Kind Regards,
Volker

On Sun, Jun 8, 2025 at 6:18 AM Larry Garfield <larry@garfieldtech.com>
wrote:

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


-- 
Volker Dusch
Head of Engineering
Tideways GmbH
Königswinterer Str. 116
53227 Bonn
https://tideways.io/imprint

Sitz der Gesellschaft: Bonn
Geschäftsführer: Benjamin Außenhofer (geb. Eberlei)
Registergericht: Amtsgericht Bonn, HRB 22127

---

## #127624 — Nick — 2025-06-08T13:27:24+00:00

URL: https://externals.io/message/127621#127624 · In reply to: #127622

Hey Volker,

> 
> 
> a) From the "ProductFromDB" i get:
> 
> > […]
> 
> 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

---

## #127629 — Claude Pache — 2025-06-09T09:51:44+00:00

URL: https://externals.io/message/127621#127629 · In reply to: #127621

> Le 8 juin 2025 à 06:16, Larry Garfield <larry@garfieldtech.com> a écrit :
> 
> As Nick has graciously provided an implementation, we would like to open discussion on this very small RFC to allow `readonly` on backed properties even if they have a hook defined.
> 
> https://wiki.php.net/rfc/readonly_hooks
> 
> -- 
>  Larry Garfield
>  larry@garfieldtech.com


Hi Larry, Nick,

Last summer, the question of allowing hooks on readonly has been raised as part of the RFC «Property hooks improvements», and at that time I have raised an objection on allowing the get hook on readonly properties and I have suggested for a better design for the main issue it was supposed to solve, see https://externals.io/message/124149#124187 and the following messages. (The RFC itself was trimmed down to the non-controversial part.) I’ll repeat here both my objection and my proposal for better design, but more strongly, with the hope that the message will be received.

The purpose of readonly properties is (citing the original RFC, https://wiki.php.net/rfc/readonly_properties_v2#rationale) to provide strong immutable guarantee, i.e.:

```php
class Test {
    public readonly string $prop;
 
    public function method(Closure $fn) {
        $prop = $this->prop;
        $fn(); // Any code may run here.
        $prop2 = $this->prop;
        assert($prop === $prop2); // Always holds.
    }
}
```

By allowing a get hook on readonly property, you are effectively nullifying this invariant. Invariants must be enforced be the engines (whenever possible; there is an inevitable loophole until the property is initialised), and not left to the discretion of the user. If a get hook on readonly property is allowed, a random user will use its creativity in order to circumvent the intended invariant (recall: immutability). I say “creativity”, not “dumbness”, because you cannot mechanically tell the two apart:

```php
class doc {
    public readonly int page {
        get => $this->page + $this->offset;
    }
    private int $offset = 0;
    public function __construct(int $page) {
        $this->page = $page;
    }
    public function foo() {
        // $this->offset may be adjusted here
    }
}
```

I know that some people won’t see a problem with that code (see the cited thread above), and this is a strong reason not to allow that: you cannot trust the user to enforce invariants that they don’t understand or are not interested in.

(The objection above is for the `get` hook`; there is no such issue with the `set` hook.)

Now, here is the suggestion for a better alternative design, that (1) don’t allow to break the invariant of immutability, (2) solve the issue of lazy initialisation (which is, I guess, the main purpose of the `get` hook on readonly), and (3) also works with nullable properties:

Add an additional hook to backed properties, named `init`. When attempting to read the value of the backing store, if it is uninitialised, then the init hook is triggered, which is supposed to initialise it.

—Claude

---

## #127633 — Larry Garfield — 2025-06-09T15:11:42+00:00

URL: https://externals.io/message/127621#127633 · In reply to: #127622

> Hey Larry,
>
> Couple points from a first read and from trying to run the examples.
>
> a) From the "ProductFromDB" i get:
>
> > […]
>
> I assume you're missing a return statement here? Or is there something 
> I'm missing?
>
> b) Minor wording gripe in the proposal section
>
> > […]
>
> 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) 
>
> > […]
>
> 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

---

## #127809 — Larry Garfield — 2025-06-30T17:35:16+00:00

URL: https://externals.io/message/127621#127809 · In reply to: #127621

> As Nick has graciously provided an implementation, we would like to 
> open discussion on this very small RFC to allow `readonly` on backed 
> properties even if they have a hook defined.
>
> https://wiki.php.net/rfc/readonly_hooks

After some back and forth on the PR to settle on error messages, this RFC seems ready.  Baring any other feedback we will open the vote on it sometime on Wednesday.

--Larry Garfield

---

## #127823 — tim@bastelstu.be — 2025-07-01T14:27:15+00:00

URL: https://externals.io/message/127621#127823 · In reply to: #127633

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

---

## #127958 — Larry Garfield — 2025-07-08T15:10:33+00:00

URL: https://externals.io/message/127621#127958 · In reply to: #127823

> Hi
>
> Am 2025-06-09 17:11, schrieb Larry Garfield:
> > […]
>
> 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

---

## #127959 — Nicolas Grekas — 2025-07-08T15:32:44+00:00

URL: https://externals.io/message/127621#127959 · In reply to: #127958

Hi Larry et al.

Le mar. 8 juil. 2025 à 17:12, Larry Garfield <larry@garfieldtech.com> a
écrit :

> 
> > […]
>
> 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.
>
> > […]
>
> 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.

---

## #127962 — Nicolas Grekas — 2025-07-09T06:30:31+00:00

URL: https://externals.io/message/127621#127962 · In reply to: #127823

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 don't think this really resolves Claude's concern. While it is
> certainly true that the guarantees do not currently hold, I don't
> believe this is strong enough of a reason not to provide for stronger
> guarantees in a *newly introduced feature*. The point of property hooks
> is for me that “dynamic properties” are easier to reason about compared
> to `__get()`. As a user when accessing a proper `readonly` property, I
> do not want to check if there is a property hook that might result in
> non-readonly behavior. As an engine developer I want to be able to
> optimize based on the `readonly`-ness of a property. Without such
> guarantees, the “readonly” keyword does not provide value to me.
>
> I also believe the LazyProduct example to be broken, since lazy-loading
> individual properties might result in an object that is internally
> consistent if the database changes in-between.


Here are two situations that are perfectly valid use cases for the example:
- event-sourced / versioned entities in the DB, where the state of an
object cannot change in the backend
- lazy-parsed network payloads, where one parses only part of some JSONs on
demand (mongodb and symfony/json-streamer do such things already)

Nicolas

---

## #127966 — Claude Pache — 2025-07-09T10:52:48+00:00

URL: https://externals.io/message/127621#127966 · In reply to: #127959

> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit :
> 
> I read Claude's concern, and I agree with Larry's response: the engine already allows readonly to be bypassed using __get. The added hook doesn't make anything more lenient.
> 

It is true that readonly could be bypassed by __get(); but this is a legacy behaviour, and you have to take an explicit step to make it possible. For those unaware of the awful hack, here is a minimal test case:

https://3v4l.org/N78An

where the `unset(...)` is mandatory to make it “work”.

Are we obligated to sanction shortcomings of legacy concepts in newly introduced concepts that are supposed to replace them? Or can we do something better? I’ve outlined in a previous email what I think is a better design for such situation (namely an `init` hook).

Also, the fact that __get() is not yet deprecated means that we can still use the aforementioned hack until/unless we’ve implemented a proper solution. In the worst case, you can still use a non-readonly hooked property and document the intended invariants in phpdoc.


> If a class is final and uses readonly with either hooks or __get, then that's the original author's choice. There's no need for extra engine-assisted strictness in this case. You cannot write such code in a non-readonly way by mistake, so it has to be by intent.
> 

Enforcing as strictly as possible its intended invariants is a good design for a robust language. Yes, it implies that users cannot (or can hardly) escape annoying constraints. For example, you can’t extend a final class, even if you think that you have good reason for it, like constructing a mock object.

—Claude

---

## #127967 — Nicolas Grekas — 2025-07-09T11:39:35+00:00

URL: https://externals.io/message/127621#127967 · In reply to: #127966

Thanks for your detailed thoughts, Claude. I'd like to offer my perspective
on some of the points you raised.

Le mer. 9 juil. 2025 à 12:53, Claude Pache <claude.pache@gmail.com> a
écrit :

>
>
> Le 8 juil. 2025 à 17:32, Nicolas Grekas <nicolas.grekas+php@gmail.com> a
> écrit :
>
> I read Claude's concern, and I agree with Larry's response: the engine
> already allows readonly to be bypassed using __get. The added hook doesn't
> make anything more lenient.
>
>
> It is true that readonly could be bypassed by __get(); but this is a
> legacy behaviour, and you have to take an explicit step to make it
> possible. For those unaware of the awful hack, here is a minimal test case:
>
> https://3v4l.org/N78An
>
> where the `unset(...)` is mandatory to make it “work”.
>
> Are we obligated to sanction shortcomings of legacy concepts in newly
> introduced concepts that are supposed to replace them? Or can we do
> something better? I’ve outlined in a previous email what I think is a
> better design for such situation (namely an `init` hook).
>
> Also, the fact that __get() is not yet deprecated means that we can still
> use the aforementioned hack until/unless we’ve implemented a proper
> solution. In the worst case, you can still use a non-readonly hooked
> property and document the intended invariants in phpdoc.
>


__get is certainly not legacy; removing it would break many use cases
without proper alternatives.
The behavior after unset() has been promoted to a language feature when
readonly properties were introduced *because* it helps solve real world use
cases.
I've been asked recently by Gina if those use cases were covered by eg
native lazy proxies. The answer is *no*, because native lazy proxies cover
only part of the lazy-proxying domain: what remains is proxying by
interface and proxying internal classes, and those require a way to proxy
all property accesses, which is why magic methods are required.

With the argument that __get can be used to implement the
non-readonly-ness, we could also say that hooks are not needed, because
they can be implemented using __get. Yet, language aesthetics are
important, and we welcomed hooks for this reason. Being able to easily
lazy-init thanks to hooks on readonly would be a welcome improvement to me.

That being said, about your init proposal, I think that could work. I'd
just do it a bit differently: instead of introducing a new "init" hook, I'd
prefer having "set" mean "init" for readonly properties. But I know nothing
about the engine on the topic so I can't comment on the feasibility aspect.
I'll leave this to others.

Just a word about using hooks vs __get for lazy-init: the really hard part
when using __get is emulating the public/protected/private visibility
rules. Hooks make this a non-issue. Yet hooks - unfortunately - can't be
used as a generic lazy-init implementation because of their behavior
related to references. That's another topic, but still related, to
reinforce that __get is certainly not legacy.



>
> If a class is final and uses readonly with either hooks or __get, then
> that's the original author's choice. There's no need for extra
> engine-assisted strictness in this case. You cannot write such code in a
> non-readonly way by mistake, so it has to be by intent.
>
>
> Enforcing as strictly as possible its intended invariants is a good design
> for a robust language. Yes, it implies that users cannot (or can hardly)
> escape annoying constraints. For example, you can’t extend a final class,
> even if you think that you have good reason for it, like constructing a
> mock object.
>


That's not strictness when the root concept is already filled with
conceptual holes... I'm surprised nobody ever proposed the concept of an
*immutable* keyword, that'd be like readonly but that'd accept only
also-immutable values. Until this happens, using readonly for that is
a fallacy I'm sorry... To me that invalidates all related arguments.

Nicolas

---

## #127969 — Nick — 2025-07-09T13:17:47+00:00

URL: https://externals.io/message/127621#127969 · In reply to: #127966

Hey Claude,

> > […]
> 
> It is true that readonly could be bypassed by __get(); but this is a legacy behaviour, and you have to take an explicit step to make it possible. For those unaware of the awful hack, here is a minimal test case:
> 
> https://3v4l.org/N78An
> 
> where the `unset(...)` is mandatory to make it “work”.
> 
> Are we obligated to sanction shortcomings of legacy concepts in newly introduced concepts that are supposed to replace them? Or can we do something better? I’ve outlined in a previous email what I think is a better design for such situation (namely an `init` hook).
> 
> Also, the fact that __get() is not yet deprecated means that we can still use the aforementioned hack until/unless we’ve implemented a proper solution. In the worst case, you can still use a non-readonly hooked property and document the intended invariants in phpdoc.
> 
> 
> > […]
> 
> 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)

---

## #127972 — Larry Garfield — 2025-07-09T14:05:41+00:00

URL: https://externals.io/message/127621#127972 · In reply to: #127966

> > […]
>
> It is true that readonly could be bypassed by __get(); but this is a 
> legacy behaviour, and you have to take an explicit step to make it 
> possible. For those unaware of the awful hack, here is a minimal test 
> case:
>
> https://3v4l.org/N78An
>
> where the `unset(...)` is mandatory to make it “work”.
>
> Are we obligated to sanction shortcomings of legacy concepts in newly 
> introduced concepts that are supposed to replace them? Or can we do 
> something better? I’ve outlined in a previous email what I think is a 
> better design for such situation (namely an `init` hook).
>
> Also, the fact that __get() is not yet deprecated means that we can 
> still use the aforementioned hack until/unless we’ve implemented a 
> proper solution. In the worst case, you can still use a non-readonly 
> hooked property and document the intended invariants in phpdoc.
>
>
> > […]
>
> 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

---

## #127974 — Eric Norris — 2025-07-09T15:42:36+00:00

URL: https://externals.io/message/127621#127974 · In reply to: #127972

> 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.

---

## #127976 — Rob Landers — 2025-07-09T16:22:21+00:00

URL: https://externals.io/message/127621#127976 · In reply to: #127967

> 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 :
> > […]
> 
> 
> __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.
>  
> 
> > […]
> 
> 
> 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

---

## #127979 — Larry Garfield — 2025-07-09T17:11:29+00:00

URL: https://externals.io/message/127621#127979 · In reply to: #127974

> > […]
>
> 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

---

## #127980 — Claude Pache — 2025-07-09T17:58:36+00:00

URL: https://externals.io/message/127621#127980 · In reply to: #127969

> Le 9 juil. 2025 à 15:17, Nick <php@nicksdot.dev> a écrit :
> 
> Hey Claude,
> 
> 
> I hear you, but I still struggle to fully grasp the issue. It’s genuinely hard for me to come up with a real-world example that actually makes sense.
> Everything I’ve seen so far, including the RFC example and what I tried myself (I gave it an honest shot), feels either very theoretical or entirely intentional, and thus perfectly logical in its outcome.
> 
> In one of your previous mails you brought up an example that requires calling a class method (read: intentionally changing class state), which  would result in a non-consistent value being returned when calling the same property more than once. I get it. But what if the user wants exactly that in their `readonly` class?

Yes, it’s mostly theoretical, but it is good to base language design on sound theory.

But here is a potential practical issue. A random user wants to extend a class from a third-party library, but they are annoyed that a given property is readonly. Now, using a get hook, it is trivial for them to cheat and to work around what it perceives as an undue limitation, not realising that it may break assumptions made elsewhere in the library.  — Indeed, I don’t trust users and want to protect them against themselves.

> 
> That said I did address your concern here (actual RFC PR branch against alternative; PoC):
> https://github.com/NickSdot/php-php-src/compare/allow-readonly-hooks...NickSdot:php-php-src:readonly-hooks-strict
> 
> Larry and I agree that we don’t want this complexity in the current RFC.
> Perhaps this is something for a separate `init` hook RFC?

I think indeed that it is not worth making the current proposal more complex, but rather considering whether implementing an init hook instead is a reasonable alternative.

Also there is another issue with the use of get hook for lazy initialisation (although not specific to readonly): The `??=` pattern breaks if the property is nullable and you initialise it to `null`. It is in fact cumbersome to distinguish between an uninitialised property and a property initialised with null.

—Claude

---

## #127988 — tim@bastelstu.be — 2025-07-10T10:37:57+00:00

URL: https://externals.io/message/127621#127988 · In reply to: #127958

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

---

## #127989 — tim@bastelstu.be — 2025-07-10T10:43:45+00:00

URL: https://externals.io/message/127621#127989 · In reply to: #127959

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

---

## #127997 — Larry Garfield — 2025-07-10T15:34:15+00:00

URL: https://externals.io/message/127621#127997 · In reply to: #127989

> Hi
>
> Am 2025-07-08 17:32, schrieb Nicolas Grekas:
> > […]
>
> 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.
>
> > […]
>
> That is saying "it's impossible to introduce bugs".
>
> > […]
>
> `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

---

## #127998 — Rob Landers — 2025-07-10T18:43:38+00:00

URL: https://externals.io/message/127621#127998 · In reply to: #127997

> 
> > […]
> 
> Nick previously suggested having the get-hook's first return value cached; it would still be subsequently called, so any side effects would still happen (though I don't know why you'd want side effects), but only the first returned value would ever get returned.  Would anyone find that acceptable?  (In the typical case, it would be the same as the current $this->foo ??= compute() pattern, just with an extra cache entry.)
> 
> --Larry Garfield
> 

I think that only covers one use-case for getters on readonly classes. Take this example for discussion:

readonly class User {
    public int $elapsedTimeSinceCreation { get => `time()` - $this->createdAt; }
    private int $cachedResult;
    public int $totalBalance { get => $this->cachedResult ??= 5+10; }
    public int $accessLevel { get => getCurrentAccessLevel(); }
    public function __construct(public int $createdAt) {}
}

$user = new User(time() - 5);
var_dump($user->elapsedTimeSinceCreation); // 5
var_dump($user->totalBalance); // 15
var_dump($user->accessLevel); // 42

In this example, we have three of the most common ones:
 1. Computed Properties (elapsedTimeSinceCreation): these are properties of the object that are relevant to the object in question, but are not static. In this case, you are not writing to the object. It is still "readonly".
 2. Memoization (expensiveCalculation): only calculate the property once and only once. This is a performance optimization. It is still "readonly".
 3. External State (accessLevel): properties of the object that rely on some external state, which due to architecture or other convienence may not make sense as part of object construction. It is still "readonly".
You can mix-and-match these to provide your own level of immutability, but memoization is certainly not the only one. 

You could make the argument that these should be functions, but I'd posit that these are properties of the user object. In other words, a function to get these values would probably be named `getElapsedTimeSinceCreation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway.

— Rob

---

## #127999 — Nick — 2025-07-11T00:40:21+00:00

URL: https://externals.io/message/127621#127999 · In reply to: #127998

Hey Rob,

> 
> > […]
> 
> I think that only covers one use-case for getters on readonly classes. Take this example for discussion:
> 
> readonly class User {
>     public int $elapsedTimeSinceCreation { get => `time()` - $this->createdAt; }
>     private int $cachedResult;
>     public int $totalBalance { get => $this->cachedResult ??= 5+10; }
>     public int $accessLevel { get => getCurrentAccessLevel(); }
>     public function __construct(public int $createdAt) {}
> }
> 
> $user = new User(time() - 5);
> var_dump($user->elapsedTimeSinceCreation); // 5
> var_dump($user->totalBalance); // 15
> var_dump($user->accessLevel); // 42
> 
> In this example, we have three of the most common ones:
> Computed Properties (elapsedTimeSinceCreation): these are properties of the object that are relevant to the object in question, but are not static. In this case, you are not writing to the object. It is still "readonly".
> Memoization (expensiveCalculation): only calculate the property once and only once. This is a performance optimization. It is still "readonly".
> External State (accessLevel): properties of the object that rely on some external state, which due to architecture or other convienence may not make sense as part of object construction. It is still "readonly".
> You can mix-and-match these to provide your own level of immutability, but memoization is certainly not the only one. 
> 
> You could make the argument that these should be functions, but I'd posit that these are properties of the user object. In other words, a function to get these values would probably be named `getElapsedTimeSinceCreation()`, `getTotalBalance`, or `getAccessLevel` -- we'd be writing getters anyway.
> 
> — Rob

Please remember that the RFC will allow `readonly` on backed properties only, not on virtual hooked properties. 
Nothing from your example would work, and it would result in:
Fatal error: Hooked virtual properties cannot be declared readonly
My proposed alternative implementation with caching addresses the concern Claude and Tim had and will make this hold:

```php
class Unusual
{
    public function __construct(
        public readonly int $value {
            get => $this->value * random_int(1, 100);
        }
    ) {}
}

$unusual = new Unusual(1);
var_dump($unusual->value === $unusual->value); // true 
```

– Nick

---

## #128000 — Eric Norris — 2025-07-11T03:08:40+00:00

URL: https://externals.io/message/127621#128000 · In reply to: #127979

>
> 
> > […]
>
> 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?

---

## #128001 — Nick — 2025-07-11T04:20:57+00:00

URL: https://externals.io/message/127621#128001 · In reply to: #127988

Hey Tim,

> 
> 
> Hi
> 
> Am 2025-07-08 17:10, schrieb Larry Garfield:
> > […]
> 
> Or by not allowing a `get` hook on readonly properties, of course.
> 
> Best regards
> Tim Düsterhus

Personally, I would really like to have `get` hooks on readonly properties. Please consider something like this:

```php
readonly class Foo
{
    public function __construct(
        public Output $style,
        public string $some {
            get => Output::One === $this->style ? ucfirst($this->some) : strtoupper($this->some);
            set => '' !== $value ? $value : throw new \Exception();
        }
    ) {}
}
```

Easy-to-digest one-liners. Concerns remain separated. Set takes care of validation, get formats.
If `get` would not be allowed, we couldn’t do such an obvious thing. For what reason?
Instead we would need to delegate formatting to the `set` hook which is messy.

```php
readonly class Foo
{
    public function __construct(
        public Output $style,
        public string $some {
            set => '' !== $value ? (Output::One === $this->style ? ucfirst($value) : strtoupper($value)) : throw new \Exception();
        }
    ) {}
}
```

Now that I have proposed alternative implementations with caching, I don’t see why `get` should not be allowed.

Cheers,
Nick

Aside: I added two links to alternative implementations to the PR description.

---

Next page: https://externals.io/message/127621.md?page=2
