[PHP-DEV] [RFC] Allow Reassignment of Promoted Readonly Properties in Constructor

On Tue, Feb 3, 2026, at 10:22, Nicolas Grekas wrote:

Le mar. 3 févr. 2026 à 10:00, Rob Landers rob@bottled.codes a écrit :

On Tue, Feb 3, 2026, at 09:56, Nicolas Grekas wrote:

Hi Rob,

Le mar. 3 févr. 2026 à 09:50, Rob Landers rob@bottled.codes a écrit :

On Mon, Feb 2, 2026, at 22:14, Nicolas Grekas wrote:

Hi Marco,

Le lun. 2 févr. 2026 à 11:54, Marco Pivetta <ocramius@gmail.com> a écrit :

Hey Nicolas,

On Thu, 22 Jan 2026 at 16:34, Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Dear all,

Here is a new RFC for you to consider:
https://wiki.php.net/rfc/promoted_readonly_constructor_reassign

What happens if one calls $obj->__construct(1, 2, 3) (on an already instantiated $obj) in the context of this patch?

Thanks for asking, I didn’t think about this. This made me also think about ReflectionClass::newInstanceWithoutConstructor().
I clarified this in the RFC, see “Direct __construct() Calls Cannot Bypass Readonly” and “Reflection: Objects Created Without Constructor”.
Patch and PR updated also if anyone wants to run some code where this RFC can be played with.

Cheers,
Nicolas

Hi Nicolas,

Under “Child Classes Can Reassign Parent Properties”: this feels like a major footgun. Calling parent::__construct() won’t allow a reset (per the rules of calling a constructor directly); which would completely break inheritance… but then in the examples it says that calling a constructor directly can reset it – but you can’t?

This feels really inconsistent to me.

— Rob

Yes, the text was ambiguous. The implementation allows parent::__construct() during the initial construction (normal inheritance), and only blocks explicit __construct() calls after construction completed. I’ve clarified this in the RFC.

Nicolas

Will this result in a catchable error? I assume so, so a valid pattern during inheritance might be to put these in a try/catch so children can set them first?

FWIW, in my Records RFC, properties were fully mutable during construction for exactly this reason.

The existing behavior is preserved: if a reassignment fails, it throws a catchable Error. The implicit CPP assignment in a parent constructor happens before the parent body, so a child cannot “set first” and then call ‘‘parent::__construct()’’ to avoid it; a try/catch in the parent cannot intercept it. But a try/catch in the child can catch of course.

Does that answer your question?

So, this could end up with partial application of state? Or does it rollback? For example:

class Box {
public function __construct(readonly int $x, readonly int $y, readonly bool $isSquare = false) {
$this->isSquare = $x == $y;
}
}

class Square extends Box {
public function __construct(readonly int $size) {
$this->isSquare = true;
try {
parent::__construct($size, $size); // what is the state after it throws?
} catch(\Throwable) {}
}
}

(I spent over a year thinking about this stuff … so if you’re interested in more edge cases, I can dig up my notes)

— Rob

Hi Rob,

Le mar. 3 févr. 2026 à 09:50, Rob Landers rob@bottled.codes a écrit :

On Mon, Feb 2, 2026, at 22:14, Nicolas Grekas wrote:

Hi Marco,

Le lun. 2 févr. 2026 à 11:54, Marco Pivetta <ocramius@gmail.com> a écrit :

Hey Nicolas,

On Thu, 22 Jan 2026 at 16:34, Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Dear all,

Here is a new RFC for you to consider:
https://wiki.php.net/rfc/promoted_readonly_constructor_reassign

What happens if one calls $obj->__construct(1, 2, 3) (on an already instantiated $obj) in the context of this patch?

Thanks for asking, I didn’t think about this. This made me also think about ReflectionClass::newInstanceWithoutConstructor().
I clarified this in the RFC, see “Direct __construct() Calls Cannot Bypass Readonly” and “Reflection: Objects Created Without Constructor”.
Patch and PR updated also if anyone wants to run some code where this RFC can be played with.

Cheers,
Nicolas

Hi Nicolas,

Under “Child Classes Can Reassign Parent Properties”: this feels like a major footgun. Calling parent::__construct() won’t allow a reset (per the rules of calling a constructor directly); which would completely break inheritance… but then in the examples it says that calling a constructor directly can reset it – but you can’t?

This feels really inconsistent to me.

— Rob

Yes, the text was ambiguous. The implementation allows parent::__construct() during the initial construction (normal inheritance), and only blocks explicit __construct() calls after construction completed. I’ve clarified this in the RFC.

Nicolas

Le mar. 3 févr. 2026 à 10:30, Rob Landers rob@bottled.codes a écrit :

On Tue, Feb 3, 2026, at 10:22, Nicolas Grekas wrote:

Le mar. 3 févr. 2026 à 10:00, Rob Landers rob@bottled.codes a écrit :

On Tue, Feb 3, 2026, at 09:56, Nicolas Grekas wrote:

Hi Rob,

Le mar. 3 févr. 2026 à 09:50, Rob Landers rob@bottled.codes a écrit :

On Mon, Feb 2, 2026, at 22:14, Nicolas Grekas wrote:

Hi Marco,

Le lun. 2 févr. 2026 à 11:54, Marco Pivetta <ocramius@gmail.com> a écrit :

Hey Nicolas,

On Thu, 22 Jan 2026 at 16:34, Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

Dear all,

Here is a new RFC for you to consider:
https://wiki.php.net/rfc/promoted_readonly_constructor_reassign

What happens if one calls $obj->__construct(1, 2, 3) (on an already instantiated $obj) in the context of this patch?

Thanks for asking, I didn’t think about this. This made me also think about ReflectionClass::newInstanceWithoutConstructor().
I clarified this in the RFC, see “Direct __construct() Calls Cannot Bypass Readonly” and “Reflection: Objects Created Without Constructor”.
Patch and PR updated also if anyone wants to run some code where this RFC can be played with.

Cheers,
Nicolas

Hi Nicolas,

Under “Child Classes Can Reassign Parent Properties”: this feels like a major footgun. Calling parent::__construct() won’t allow a reset (per the rules of calling a constructor directly); which would completely break inheritance… but then in the examples it says that calling a constructor directly can reset it – but you can’t?

This feels really inconsistent to me.

— Rob

Yes, the text was ambiguous. The implementation allows parent::__construct() during the initial construction (normal inheritance), and only blocks explicit __construct() calls after construction completed. I’ve clarified this in the RFC.

Nicolas

Will this result in a catchable error? I assume so, so a valid pattern during inheritance might be to put these in a try/catch so children can set them first?

FWIW, in my Records RFC, properties were fully mutable during construction for exactly this reason.

The existing behavior is preserved: if a reassignment fails, it throws a catchable Error. The implicit CPP assignment in a parent constructor happens before the parent body, so a child cannot “set first” and then call ‘‘parent::__construct()’’ to avoid it; a try/catch in the parent cannot intercept it. But a try/catch in the child can catch of course.

Does that answer your question?

So, this could end up with partial application of state? Or does it rollback? For example:

class Box {
public function __construct(readonly int $x, readonly int $y, readonly bool $isSquare = false) {
$this->isSquare = $x == $y;
}
}

class Square extends Box {
public function __construct(readonly int $size) {
$this->isSquare = true;
try {
parent::__construct($size, $size); // what is the state after it throws?
} catch(\Throwable) {}
}
}

(I spent over a year thinking about this stuff … so if you’re interested in more edge cases, I can dig up my notes)

You’re correct. Although such code explicitly decided to opt-in for ignoring anything from the parent, and that’s always a very bad idea.

FWIW, in my Records RFC, properties were fully mutable during construction for exactly this reason.

I wouldn’t mind making readonly properties mutable during initial construction if we can get a consensus on this.
I’ve not been brave enough to consider this was possible. I might be wrong :smiley:

Hi

On 2/3/26 10:22, Nicolas Grekas wrote:

The existing behavior is preserved: if a reassignment fails, it throws a
catchable Error. The implicit CPP assignment in a parent constructor
happens before the parent body, so a child cannot "set first" and then call
''parent::__construct()'' to avoid it; a try/catch in the parent cannot

This is a good example that as far as I can tell is not explicitly spelled out in the RFC: Please include an example where the *child* sets a readonly property defined in the parent before calling the parent constructor.

     class P {
         public function __construct(
             public readonly string $x = 'P',
         ) { }
     }

     class C extends P {
         public function __construct() {
             $this->x = 'C';

             parent::__construct(); // Will this throw or not?
         }
     }

More generally, as Rob, I stumbled upon the “Child Classes Can Reassign Parent Properties” section, because it's least obviously correct behavior to me.

My understanding of this RFC is that it is intended to solve the case where the class itself needs to perform some “post-processing” on a promoted readonly property that it owns. Specifically, the property becomes locked once the constructor completes.

For the example in “Child Classes Can Reassign Parent Properties” my mental model says that `$prop` is owned by `Parent_`, since `Parent_` is the class that declared it. Thus it would be natural for me to ”lock” `$prop` once the `parent::__construct()` call completes. If the child class needs to do special processing on the property, it has two options:

1. Not call the parent constructor. If the parent constructor logic is unfit, then not calling the constructor is the right thing rather than trying to revert part of what it did to a readonly property.

2. Call `parent::__construct()` with an appropriately modified value:
parent::__construct('child override');

So to describe my expected semantics in more technical terms: The implementation should “unlock” the property after the “auto-generated promotion logic” finished and should “relock” the property when the method with the auto-generated promotion logic finishes.

In other words:

     public function __construct(
         public readonly string $prop = 'parent default',
     ) {
         // Parent does NOT reassign here
     }

should be equivalent to:

     public function __construct(
         string $prop = 'parent default',
     ) {
         $this->prop = $prop;
         // unlock $this->prop (set IS_PROP_REINITABLE)
         try {
             // Parent does NOT reassign here
         } finally {
             // lock $this->prop (unset IS_PROP_REINITABLE)
         }
     }

With this logic, the answer to initial “will this throw” question of this email would be “yes”, because the implicit `$this->prop = $prop` assignment happens before the unlock. I believe it would also more closely match the semantics of `__clone()`.

Best regards
Tim Düsterhus