# [RFC] Readonly Property Defaults

- Thread: https://externals.io/message/131760
- Original archive: https://news-web.php.net/php.internals/131760
- Started by Nick Sdot on 2026-07-04T10:03:48+00:00
- Messages: 14
- RFC: Readonly Property Defaults (Implemented (PHP 8.6)) — https://externals.io/rfc/readonly_property_defaults.md
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #131760 — Nick Sdot — 2026-07-04T10:03:48+00:00

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

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: 
https://wiki.php.net/rfc/readonly_property_defaults

--

Cheers
Nick

---

## #131830 — tim@bastelstu.be — 2026-07-09T09:12:09+00:00

URL: https://externals.io/message/131760#131830 · In reply to: #131760

Hi


> 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: 
> https://wiki.php.net/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

---

## #131838 — Nick Sdot — 2026-07-09T11:58:53+00:00

URL: https://externals.io/message/131760#131838 · In reply to: #131830

Hey Tim,

thanks for the feedback!


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

---

## #131840 — tim@bastelstu.be — 2026-07-09T13:12:31+00:00

URL: https://externals.io/message/131760#131840 · In reply to: #131838

Hi


> 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

---

## #131844 — Nick Sdot — 2026-07-09T19:18:05+00:00

URL: https://externals.io/message/131760#131844 · In reply to: #131840

Hey Tim


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

---

## #131862 — Larry Garfield — 2026-07-10T23:02:19+00:00

URL: https://externals.io/message/131760#131862 · In reply to: #131760

> 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: 
> https://wiki.php.net/rfc/readonly_property_defaults

I had been planning to say "meh, doesn't seem like it would hurt anything," but as Tim pointed out with unserialize, it potentially could hurt something.  That makes me skeptical.

I also want to call out, as the RFC notes, that this is already a solved problem with private(set).  It's not quite the same as readonly, but for most practical purposes it's close enough and already addresses the use case this RFC.  

--Larry Garfield

---

## #131863 — Nick Sdot — 2026-07-11T07:51:40+00:00

URL: https://externals.io/message/131760#131863 · In reply to: #131862

Hey Larry,


> 
> > […]
> I had been planning to say "meh, doesn't seem like it would hurt anything," but as Tim pointed out with unserialize, it potentially could hurt something.  That makes me skeptical.
>
> I also want to call out, as the RFC notes, that this is already a solved problem with private(set).  It's not quite the same as readonly, but for most practical purposes it's close enough and already addresses the use case this RFC.


Thanks for the feedback.

I have to push back. It is not a solved problem. In fact, `private(set)` 
is *not* only "not quite the same". Both features solve fundamentally 
different problems -- `private(set)` has a foot gun `readonly` does not 
have.

Because this point comes up repeatedly here is a concrete example:

```
final class PipelineOne // private(set)

{
     public private(set)array $orderedPipelineSteps = [
         \Thing\StepDoSomethingTwo::class,
         \Thing\StepAtReportBeginning::class,
         \Thing\StepDoSomethingOne::class,
     ];public function describe(): array {\sort($this->orderedPipelineSteps);// whoopsie, should have copied first $ordered = $this->orderedPipelineSteps;

         // do stuff return $ordered;
     }
}

final readonly class PipelineTwo// readonly{
     public function __construct(public array $orderedPipelineSteps = [
             \Thing\StepDoSomethingTwo::class,
             \Thing\StepAtReportBeginning::class,
             \Thing\StepDoSomethingOne::class,
         ],
     ) {}public function describe(): array {\sort($this->orderedPipelineSteps);// whoopsie, should have copied first $ordered = $this->orderedPipelineSteps;

         // do stuff return $ordered;
     }
}

// ###################### private(set) ###################### $one = new 
PipelineOne();
\var_dump($one->describe());
// array(3) { // [0]=> // string(27) "Thing\StepAtReportBeginning" // 
good, now ordered // [1]=> // string(24) "Thing\StepDoSomethingOne" // 
[2]=> // string(24) "Thing\StepDoSomethingTwo" //} \var_dump($one->orderedPipelineSteps);
// array(3) { // [0]=> // string(27) "Thing\StepAtReportBeginning" // 
whoopsie, this should NOT be first! // [1]=> // string(24) 
"Thing\StepDoSomethingOne" // [2]=> // string(24) 
"Thing\StepDoSomethingTwo" //} // ###################### readonly 
###################### $two = new PipelineTwo();
\var_dump($two->describe());
//Fatal error: Uncaught Error: Cannot indirectly modify readonly 
property PipelineTwo::$orderedPipelineSteps

```

With `private(set)` accidental mutation is legal. With `readonly`, 
modification after initialisation is illegal. Both are great and useful 
features, but `private(set)` only controls visibility not mutation. As 
the above example proves, `readonly` is strict when it comes to mutation 
and would have prevented the bug while `private(set)` happily ignores it 
because mutation happens from within the class. With `readonly` it would 
have been surfaced that I did:

```
\sort($this->orderedPipelineSteps); // whoopsie, mutated

$ordered = $this->orderedPipelineSteps;
```

instead of:

```
$ordered = $this->orderedPipelineSteps;

\sort($ordered);

```

This alone is reason enough to use `readonly` over `private(set)`; less 
verbosity is a cherry on top.

Note: only to make the example copy & work without the PR branch I used 
constructor property promotion here. With the RFC, `PipelineTwo` 
can declare the property default directly, without allowing outside 
modification.

--

Cheers
Nick

---

## #131864 — Nick Sdot — 2026-07-11T08:47:07+00:00

URL: https://externals.io/message/131760#131864 · In reply to: #131862

(sorry for the double post; for some reason Thunderbird messed up 
formatting on web in the last attempt)

Hey Larry,


 > 
> > […]
 > I had been planning to say "meh, doesn't seem like it would hurt 
anything," but as Tim pointed out with unserialize, it potentially could 
hurt something.  That makes me skeptical.
 >
 > I also want to call out, as the RFC notes, that this is already a 
solved problem with private(set).  It's not quite the same as readonly, 
but for most practical purposes it's close enough and already addresses 
the use case this RFC.


Thanks for the feedback.

I have to push back. It is not a solved problem. In fact, `private(set)` 
is *not* only "not quite the same". Both features solve fundamentally 
different problems -- `private(set)` has a foot gun `readonly` does not 
have.

Because this point comes up repeatedly here is a concrete example:

```
final class PipelineOne // private(set)

{
     public private(set) array $orderedPipelineSteps = [
         \Thing\StepDoSomethingTwo::class,
         \Thing\StepAtReportBeginning::class,
         \Thing\StepDoSomethingOne::class,
     ];

     public function describe(): array
     {
         \sort($this->orderedPipelineSteps); // whoopsie, should have 
copied first

         $ordered = $this->orderedPipelineSteps;

         // do stuff

         return $ordered;
     }
}

final readonly class PipelineTwo // readonly
{
     public function __construct(
         public array $orderedPipelineSteps = [
             \Thing\StepDoSomethingTwo::class,
             \Thing\StepAtReportBeginning::class,
             \Thing\StepDoSomethingOne::class,
         ],
     ) {}

     public function describe(): array
     {
         \sort($this->orderedPipelineSteps); // whoopsie, should have 
copied first

         $ordered = $this->orderedPipelineSteps;

         // do stuff

         return $ordered;
     }
}

// ###################### private(set) ######################
$one = new PipelineOne();
\var_dump($one->describe());
// array(3) {
//  [0]=>
//  string(27) "Thing\StepAtReportBeginning" // good, now ordered
//  [1]=>
//  string(24) "Thing\StepDoSomethingOne"
//  [2]=>
//  string(24) "Thing\StepDoSomethingTwo"
//}
\var_dump($one->orderedPipelineSteps);
// array(3) {
//  [0]=>
//  string(27) "Thing\StepAtReportBeginning" // whoopsie, this should 
NOT be first!
//  [1]=>
//  string(24) "Thing\StepDoSomethingOne"
//  [2]=>
//  string(24) "Thing\StepDoSomethingTwo"
//}
// ###################### readonly ######################
$two = new PipelineTwo();
\var_dump($two->describe());
//Fatal error: Uncaught Error: Cannot indirectly modify readonly 
property PipelineTwo::$orderedPipelineSteps
```

With `private(set)` accidental mutation is legal. With `readonly`, 
modification after initialisation is illegal. Both are great and useful 
features, but `private(set)` only controls visibility not mutation. As 
the above example proves, `readonly` is strict when it comes to mutation 
and would have prevented the bug while `private(set)` happily ignores it 
because mutation happens from within the class. With `readonly` it would 
have been surfaced that I did:

```
\sort($this->orderedPipelineSteps); // whoopsie, mutated

$ordered = $this->orderedPipelineSteps;
```

instead of:

```
$ordered = $this->orderedPipelineSteps;

\sort($ordered);
```

This alone is reason enough to use `readonly` over `private(set)`; less 
verbosity is a cherry on top.

Note: only to make the example copy & work without the PR branch I used 
constructor property promotion here. With the RFC, `PipelineTwo` can 
declare the property default directly, without allowing outside 
modification.

--

Cheers
Nick

---

## #131894 — tim@bastelstu.be — 2026-07-13T17:07:10+00:00

URL: https://externals.io/message/131760#131894 · In reply to: #131844

Hi


> > […]
> 
> It would not really have prevented using `__unserialize()` on a class 
> with a readonly property with default values.

That probably depends a little on the definition of “prevent”. 
Technically you could implement `__unserialize()`, sure. But you would 
be unable to unserialize into some of properties, which the native 
unserializer would be able to.

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

Yes, I agree. clone-with already allows for the creation of object with 
a different value than the default, so allowing this for 
`__unserialize()` doesn't provide any new weird capabilities that result 
in inconsistencies.

------

As for the RFC, I have one more thing you could clarify: When using 
constructor property promotion, the default value will remain the 
default value of the parameter only and not suddenly also become the 
default of a property. It's a bit orthogonal to this RFC, and I find it 
reasonably obvious. But spelling it out to refresh everyone’s mind is 
probably a good thing nevertheless.

Other than that, I don't have any further comments on the RFC and in 
particular I don't see any more issues this would cause. Nevertheless, 
I'm probably team “Abstain” when it goes to vote :-)

Best regards
Tim Düsterhus

---

## #131896 — Nick Sdot — 2026-07-13T17:52:44+00:00

URL: https://externals.io/message/131760#131896 · In reply to: #131894

Hey Tim,


> Hi
>
> 
> > […]
>
> That probably depends a little on the definition of “prevent”. 
> Technically you could implement `__unserialize()`, sure. But you would 
> be unable to unserialize into some of properties, which the native 
> unserializer would be able to.
>
> > […]
>
> Yes, I agree. clone-with already allows for the creation of object 
> with a different value than the default, so allowing this for 
> `__unserialize()` doesn't provide any new weird capabilities that 
> result in inconsistencies.
>
> ------
>
> As for the RFC, I have one more thing you could clarify: When using 
> constructor property promotion, the default value will remain the 
> default value of the parameter only and not suddenly also become the 
> default of a property. It's a bit orthogonal to this RFC, and I find 
> it reasonably obvious. But spelling it out to refresh everyone’s mind 
> is probably a good thing nevertheless.

Not exactly sure what you would like me to spell me out. It's indeed 
different things. I made a tiny addition to the non-goals. Let me know 
if that's enough or if you had something else in mind.

> Other than that, I don't have any further comments on the RFC and in 
> particular I don't see any more issues this would cause. Nevertheless, 
> I'm probably team “Abstain” when it goes to vote :-)
>
> Best regards
> Tim Düsterhus


--

Cheers
Nick

---

## #131898 — tim@bastelstu.be — 2026-07-13T17:56:06+00:00

URL: https://externals.io/message/131760#131898 · In reply to: #131896

Hi


> Not exactly sure what you would like me to spell me out. It's indeed 
> different things. I made a tiny addition to the non-goals. Let me know 
> if that's enough or if you had something else in mind.

That works for me, thanks. The concern I had was just that “default 
values for constructor property promotion are special” and thus spelling 
it out once more that they are special, but that the RFC will not 
introduce new specialness is a good thing.

Best regards
Tim Düsterhus

---

## #131900 — Nick Sdot — 2026-07-13T18:21:39+00:00

URL: https://externals.io/message/131760#131900 · In reply to: #131898

Hey Tim,


> Hi
>
> 
> > […]
>
> That works for me, thanks. The concern I had was just that “default 
> values for constructor property promotion are special” and thus 
> spelling it out once more that they are special, but that the RFC will 
> not introduce new specialness is a good thing.
>
> Best regards
> Tim Düsterhus

Got it. Yes, no specialities. In case you deem it necessary I could add 
an assertion to an existing test. Something like:

```
class Foo { public function __construct( public readonly string $a = 
'bar') {} }
var_dump(new 
ReflectionClass(Foo::class)->getProperty('a')->hasDefaultValue()) // 
bool(false)
```

To me it feels just not too related, and in other reviews I got 
commented for over-testing.
But up to you. Feel free to let me know.

Maybe "no specialities" can "promote" you from team abstain to team yes. ;)

--
Cheers
Nick

---

## #131901 — tim@bastelstu.be — 2026-07-13T18:30:51+00:00

URL: https://externals.io/message/131760#131901 · In reply to: #131900

Hi


> Got it. Yes, no specialities. In case you deem it necessary I could add 
> an assertion to an existing test. Something like:
> 
> ```
> class Foo { public function __construct( public readonly string $a = 
> 'bar') {} }
> var_dump(new 
> ReflectionClass(Foo::class)->getProperty('a')->hasDefaultValue()) // 
> bool(false)
> ```
> 
> To me it feels just not too related, and in other reviews I got 
> commented for over-testing.
> But up to you. Feel free to let me know.

I don't think an extra test is necessary. I just want to be able to 
point folks who will inevitably run into this and raise bug reports to 
an official document that says “we thought about this, this is working 
as expected” :-) It will also be useful for the migration guide and any 
other blog post about “what's new in PHP” (particularly the less 
well-researched or AI-written ones).

> Maybe "no specialities" can "promote" you from team abstain to team 
> yes. ;)

Personally I've never had a desired to have this capability, so it will 
remain an Abstain from me to leave the decision to folks who would 
actually make use of it.

Best regards
Tim Düsterhus

---

## #132027 — Nick Sdot — 2026-07-19T12:31:14+00:00

URL: https://externals.io/message/131760#132027 · In reply to: #131760

> 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: 
> https://wiki.php.net/rfc/readonly_property_defaults
>
> -- 
>
> Cheers
> Nick
>

Hey everyone,

this is my "Intent to Vote" mail; I am planning to open the vote on or 
around July 23th.

---

Cheers
Nick
