[PHP-DEV] [RFC] Readonly property hooks

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 readonly on backed properties even if they have a hook defined.

PHP: 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

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