[PHP-DEV] [RFC] Readonly Property Defaults

Hey Internals,

I would like to present a tiny, five-line-removal RFC allowing default values on readonly properties.

Allowing default values on readonly class properties enables creating a strict contract for implementation property values that will not change at runtime -- as in, constant-like behaviour with contract. I would love doing that.

Please find the RFC text here: PHP: rfc:readonly_property_defaults

--

Cheers
Nick

Hi

On 2026-07-04 12:03, Nick Sdot wrote:

I would like to present a tiny, five-line-removal RFC allowing default values on readonly properties.

Allowing default values on readonly class properties enables creating a strict contract for implementation property values that will not change at runtime -- as in, constant-like behaviour with contract. I would love doing that.

Please find the RFC text here: PHP: rfc:readonly_property_defaults

Thank you, the proposal makes sense to me. I have just some requests for clarification:

1.

Can you add a “clone-with” example to the “Interaction with clone” section? I assume that “clone-with” will also just work for the same reasons, but it makes sense to spell it out.

2.

Can you spell out the unserialization behavior with an example. “Unserialisation continues to use the existing object hydration semantics and is not changed by this RFC” is not clear to me, because from my mental model unserialization first does a `newInstanceWithoutConstructor()` and then fills in the properties based on the serialized data. Thus a readonly field with a default value would already be initialized, which means that filling in the serialized data would fail, which I think is not a situation we have so far.

3.

In the “Interaction with asymmetric visibility” section, the example would be stronger with a `public(set)` instead of `private(set)`, because with `private(set)` the example would fail either way, since the global scope has no access to the property.

4.

Minor polishing remark in “Interaction with traits”: The opening PHP tag is missing in the example.

Best regards
Tim Düsterhus

Hey Tim,

thanks for the feedback!

On 09.07.26 16:12, Tim Düsterhus wrote:

Hi

On 2026-07-04 12:03, Nick Sdot wrote:

I would like to present a tiny, five-line-removal RFC allowing default values on readonly properties.

Allowing default values on readonly class properties enables creating a strict contract for implementation property values that will not change at runtime -- as in, constant-like behaviour with contract. I would love doing that.

Please find the RFC text here: PHP: rfc:readonly_property_defaults

Thank you, the proposal makes sense to me. I have just some requests for clarification:

1.

Can you add a “clone-with” example to the “Interaction with clone” section? I assume that “clone-with” will also just work for the same reasons, but it makes sense to spell it out.

Spelled it out and added a test for it.

2.

Can you spell out the unserialization behavior with an example. “Unserialisation continues to use the existing object hydration semantics and is not changed by this RFC” is not clear to me, because from my mental model unserialization first does a `newInstanceWithoutConstructor()` and then fills in the properties based on the serialized data. Thus a readonly field with a default value would already be initialized, which means that filling in the serialized data would fail, which I think is not a situation we have so far.

At the time `__unserialize` is called the object exists, and the property is initialised with the default value. So it fails in the same way a re-assignment in the constructor or reflection `setValue()` will fail. So, I wouldn't say "not a situation we have so far", but agree that spelling it out cannot be wrong. RFC section is updated, and your PR comment example added as a test!

3.

In the “Interaction with asymmetric visibility” section, the example would be stronger with a `public(set)` instead of `private(set)`, because with `private(set)` the example would fail either way, since the global scope has no access to the property.

You are right. Changed.

4.

Minor polishing remark in “Interaction with traits”: The opening PHP tag is missing in the example.

Fixed!

Best regards
Tim Düsterhus

--

Cheers
Nick

Hi

On 2026-07-09 13:58, Nick Sdot wrote:

At the time `__unserialize` is called the object exists, and the property is initialised with the default value. So it fails in the same way a re-assignment in the constructor or reflection `setValue()` will fail. So, I wouldn't say "not a situation we have so far", but agree that spelling it out cannot be wrong. RFC section is updated, and your PR comment example added as a test!

Yes, it would be a situation we do not yet have so far: As far as I can tell there is no way for a readonly property to be preinitialized when unserialization happens. This means that it is not not observable that the non-hooked serialization path ignores `readonly`. What is effectively guaranteed right now is that using a custom `__unserialize()` hook that loops over the given array and assigns all properties will behave identically to non-hooked serialization. This would no longer be the case as of this RFC.

There is two options here:

- The non-hooked unserialization path must also error out when trying to unserialize into a readonly property with a default value.
- __unserialize() must be able to unserialize into a readonly property with a default value, similarly to __clone().

But the current situation of “__unserialize() is less powerful than the standard unserialization” is not acceptable, because it prevents introducing `__unserialize()` on a class with a readonly property with default values.

Best regards
Tim Düsterhus

Hey Tim

On 09.07.26 20:12, Tim Düsterhus wrote:

Hi

On 2026-07-09 13:58, Nick Sdot wrote:

At the time `__unserialize` is called the object exists, and the property is initialised with the default value. So it fails in the same way a re-assignment in the constructor or reflection `setValue()` will fail. So, I wouldn't say "not a situation we have so far", but agree that spelling it out cannot be wrong. RFC section is updated, and your PR comment example added as a test!

Yes, it would be a situation we do not yet have so far: As far as I can tell there is no way for a readonly property to be preinitialized when unserialization happens. This means that it is not not observable that the non-hooked serialization path ignores `readonly`. What is effectively guaranteed right now is that using a custom `__unserialize()` hook that loops over the given array and assigns all properties will behave identically to non-hooked serialization. This would no longer be the case as of this RFC.

There is two options here:

- The non-hooked unserialization path must also error out when trying to unserialize into a readonly property with a default value.
- __unserialize() must be able to unserialize into a readonly property with a default value, similarly to __clone().

But the current situation of “__unserialize() is less powerful than the standard unserialization” is not acceptable, because it prevents introducing `__unserialize()` on a class with a readonly property with default values.

It would not really have prevented using `__unserialize()` on a class with a readonly property with default values. But fair, I don't have a strong opinion here. Given that clone-with also allows modification of already initialised readonly properties your proposed option #2 is sound.

I updated the PR and RFC

Best regards
Tim Düsterhus

--

Cheers
Nick