[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

(Reposting per Tim’s email, thanks Derick for working on this!)

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

To be sure I understood you well: you are suggesting that mutability should be scoped to the constructor that declares the property (not any constructor on the object).

This makes sense and I’ve implemented exactly that model:

  • Reassignment is allowed only while the declaring class constructor is active (including methods/closures called from it).
  • A child constructor can no longer reassign a parent-declared promoted readonly property.
  • “Child sets first, then parent::__construct()” now throws as expected.
  • The thrown Error is catchable from the child (around parent::__construct()), but not from inside the parent body before implicit CPP init.
  • Calling __construct() on an already-constructed object still cannot mutate readonly state.

I also updated the RFC text and examples to state this explicitly, and added/updated tests for the inheritance/preemption scenarios.

Anything else?

Cheers,
Nicolas

Reposting as previous delivery apparently failed.

Le ven. 13 févr. 2026, 15:52, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit :

Hi Tim,

Le jeu. 5 févr. 2026 à 20:29, Tim Düsterhus <tim@bastelstu.be> a écrit :

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

To be sure I understood you well: you are suggesting that mutability should be scoped to the constructor that declares the property (not any constructor on the object).

This makes sense and I’ve implemented exactly that model:

  • Reassignment is allowed only while the declaring class constructor is active (including methods/closures called from it).
  • A child constructor can no longer reassign a parent-declared promoted readonly property.
  • “Child sets first, then parent::__construct()” now throws as expected.
  • The thrown Error is catchable from the child (around parent::__construct()), but not from inside the parent body before implicit CPP init.
  • Calling __construct() on an already-constructed object still cannot mutate readonly state.

I also updated the RFC text and examples to state this explicitly, and added/updated tests for the inheritance/preemption scenarios.

Anything else?

Cheers,
Nicolas

Hi

On 2/16/26 19:20, Nicolas Grekas wrote:

To be sure I understood you well: you are suggesting that mutability should
be scoped to the constructor that declares the property (not any
constructor on the object).

Yes, because otherwise you might rely on implementation details of the parent constructor: Depending on whether the parent constructor reassigns internally, your reassignment in the child constructor either succeeds or fails.

This makes sense and I’ve implemented exactly that model:
- Reassignment is allowed only while the declaring class constructor is
active (including methods/closures called from it).
- A child constructor can no longer reassign a parent-declared promoted
readonly property.
- “Child sets first, then parent::__construct()” now throws as expected.
- The thrown Error is catchable from the child (around
parent::__construct()), but not from inside the parent body before implicit
CPP init.
- Calling __construct() on an already-constructed object still cannot
mutate readonly state.

I also updated the RFC text and examples to state this explicitly, and
added/updated tests for the inheritance/preemption scenarios.

Thank you. I've checked the RFC and the explanation and semantics make sense to me. I've also reviewed (parts) of the tests and provided some feedback there. I'll take another look at the tests when you made the adjustments to make sure that everything in the RFC is properly tested to make sure we didn't miss and edge case.

Anything else?

Yes, there is one edge case related to inheritance that isn't mentioned in the RFC and from what I see it's not tested either.

Child classes can redefine readonly properties and they are then “owned” by the child class. Thus we need to explain what happens in that case. I've prepared example for the three relevant cases I can think of. The follow from the existing semantics in a straight-forward fashion, but it's good to spell them out explicitly (and particularly test them).

1. Parent uses CPP, child redefines and reassigns.

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

     class C1 extends P1 {
         public readonly string $x;

         public function __construct() {
             parent::__construct();

             $this->x = 'C'; // This should fail.
         }
     }

2. Parent uses CPP and reassigns, child redefines.

     class P2 {
         public function __construct(
             public readonly string $x = 'P1',
         ) {
             $this->x = 'P2'; // This should be legal.
         }
     }

     class C2 extends P2 {
         public readonly string $x;

         public function __construct() {
             parent::__construct();
         }
     }

3. Parent uses CPP, child uses CPP redefinition.

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

     class C3 extends P3 {
         public function __construct(
             public readonly string $x = 'C1',
         ) {
             parent::__construct(); // This should fail.
         }
     }

Best regards
Tim Düsterhus

Hi Tim,

Le mer. 18 févr. 2026 à 22:29, Tim Düsterhus <tim@bastelstu.be> a écrit :

Hi

On 2/16/26 19:20, Nicolas Grekas wrote:

To be sure I understood you well: you are suggesting that mutability should
be scoped to the constructor that declares the property (not any
constructor on the object).

Yes, because otherwise you might rely on implementation details of the
parent constructor: Depending on whether the parent constructor
reassigns internally, your reassignment in the child constructor either
succeeds or fails.

This makes sense and I’ve implemented exactly that model:

  • Reassignment is allowed only while the declaring class constructor is
    active (including methods/closures called from it).
  • A child constructor can no longer reassign a parent-declared promoted
    readonly property.
  • “Child sets first, then parent::__construct()” now throws as expected.
  • The thrown Error is catchable from the child (around
    parent::__construct()), but not from inside the parent body before implicit
    CPP init.
  • Calling __construct() on an already-constructed object still cannot
    mutate readonly state.

I also updated the RFC text and examples to state this explicitly, and
added/updated tests for the inheritance/preemption scenarios.

Thank you. I’ve checked the RFC and the explanation and semantics make
sense to me. I’ve also reviewed (parts) of the tests and provided some
feedback there. I’ll take another look at the tests when you made the
adjustments to make sure that everything in the RFC is properly tested
to make sure we didn’t miss and edge case.

Anything else?

Yes, there is one edge case related to inheritance that isn’t mentioned
in the RFC and from what I see it’s not tested either.

Child classes can redefine readonly properties and they are then “owned”
by the child class. Thus we need to explain what happens in that case.
I’ve prepared example for the three relevant cases I can think of. The
follow from the existing semantics in a straight-forward fashion, but
it’s good to spell them out explicitly (and particularly test them).

  1. Parent uses CPP, child redefines and reassigns.

class P1 {
public function __construct(
public readonly string $x = ‘P’,
) { }
}

class C1 extends P1 {
public readonly string $x;

public function __construct() {
parent::__construct();

$this->x = ‘C’; // This should fail.
}
}

  1. Parent uses CPP and reassigns, child redefines.

class P2 {
public function __construct(
public readonly string $x = ‘P1’,
) {
$this->x = ‘P2’; // This should be legal.
}
}

class C2 extends P2 {
public readonly string $x;

public function __construct() {
parent::__construct();
}
}

  1. Parent uses CPP, child uses CPP redefinition.

class P3 {
public function __construct(
public readonly string $x = ‘P’,
) { }
}

class C3 extends P3 {
public function __construct(
public readonly string $x = ‘C1’,
) {
parent::__construct(); // This should fail.
}
}

Thanks, I’ve added new test cases to cover this.
I’ve also improved tests as suggested on the PR.
And finally I updated the implementation to reuse IS_PROP_REINITABLE instead of adding new flags + use an approach that doesn’t require walking the call stack.
PR and RFC updated accordingly, all green.

Nicolas

Hi everyone,

Le jeu. 19 févr. 2026 à 10:49, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit :

Hi Tim,

Le mer. 18 févr. 2026 à 22:29, Tim Düsterhus <tim@bastelstu.be> a écrit :

Hi

On 2/16/26 19:20, Nicolas Grekas wrote:

To be sure I understood you well: you are suggesting that mutability should
be scoped to the constructor that declares the property (not any
constructor on the object).

Yes, because otherwise you might rely on implementation details of the
parent constructor: Depending on whether the parent constructor
reassigns internally, your reassignment in the child constructor either
succeeds or fails.

This makes sense and I’ve implemented exactly that model:

  • Reassignment is allowed only while the declaring class constructor is
    active (including methods/closures called from it).
  • A child constructor can no longer reassign a parent-declared promoted
    readonly property.
  • “Child sets first, then parent::__construct()” now throws as expected.
  • The thrown Error is catchable from the child (around
    parent::__construct()), but not from inside the parent body before implicit
    CPP init.
  • Calling __construct() on an already-constructed object still cannot
    mutate readonly state.

I also updated the RFC text and examples to state this explicitly, and
added/updated tests for the inheritance/preemption scenarios.

Thank you. I’ve checked the RFC and the explanation and semantics make
sense to me. I’ve also reviewed (parts) of the tests and provided some
feedback there. I’ll take another look at the tests when you made the
adjustments to make sure that everything in the RFC is properly tested
to make sure we didn’t miss and edge case.

Anything else?

Yes, there is one edge case related to inheritance that isn’t mentioned
in the RFC and from what I see it’s not tested either.

Child classes can redefine readonly properties and they are then “owned”
by the child class. Thus we need to explain what happens in that case.
I’ve prepared example for the three relevant cases I can think of. The
follow from the existing semantics in a straight-forward fashion, but
it’s good to spell them out explicitly (and particularly test them).

  1. Parent uses CPP, child redefines and reassigns.

class P1 {
public function __construct(
public readonly string $x = ‘P’,
) { }
}

class C1 extends P1 {
public readonly string $x;

public function __construct() {
parent::__construct();

$this->x = ‘C’; // This should fail.
}
}

  1. Parent uses CPP and reassigns, child redefines.

class P2 {
public function __construct(
public readonly string $x = ‘P1’,
) {
$this->x = ‘P2’; // This should be legal.
}
}

class C2 extends P2 {
public readonly string $x;

public function __construct() {
parent::__construct();
}
}

  1. Parent uses CPP, child uses CPP redefinition.

class P3 {
public function __construct(
public readonly string $x = ‘P’,
) { }
}

class C3 extends P3 {
public function __construct(
public readonly string $x = ‘C1’,
) {
parent::__construct(); // This should fail.
}
}

Thanks, I’ve added new test cases to cover this.
I’ve also improved tests as suggested on the PR.
And finally I updated the implementation to reuse IS_PROP_REINITABLE instead of adding new flags + use an approach that doesn’t require walking the call stack.
PR and RFC updated accordingly, all green.

One last update following more review comments by Tim: the reassign window is now scoped to the CPP-owning constructor.
See updated wording at https://wiki.php.net/rfc/promoted_readonly_constructor_reassign

Cheers,
Nicolas

Hi

On 2/19/26 10:49, Nicolas Grekas wrote:

Thanks, I've added new test cases to cover this.
I've also improved tests as suggested on the PR.
And finally I updated the implementation to reuse IS_PROP_REINITABLE
instead of adding new flags + use an approach that doesn't require walking
the call stack.
PR and RFC updated accordingly, all green.

Thank you. The implementation looks much simpler now and the tests all make sense to me and I can't think of any other relevant “edge case”.

I have one more comment regarding the RFC text, which should result in “minor” changes as per the policy of making clarifying changes:

1. “Set in child before parent::__construct()” also fails, since the property slot is not yet initialized:

That explanation and example does not seem to be quite correct: It's not the `$this->x = 'C';` assignment that fails, it's the implicit CPP assignment when calling parent::__construct(). The explanation should be fixed and the `// Error: Cannot modify readonly property P::$x` comment should be moved to the `parent::__construct()` call.

The test in the implementation was already correct.

Other than that, I don't have any further comments and I believe everything relevant is mentioned. Personally I'm likely to “Abstain” from the vote for the same reasons that Bob previously mentioned.

Best regards
Tim Düsterhus

Le dim. 22 févr. 2026 à 19:14, Tim Düsterhus <tim@bastelstu.be> a écrit :

Hi

On 2/19/26 10:49, Nicolas Grekas wrote:

Thanks, I’ve added new test cases to cover this.
I’ve also improved tests as suggested on the PR.
And finally I updated the implementation to reuse IS_PROP_REINITABLE
instead of adding new flags + use an approach that doesn’t require walking
the call stack.
PR and RFC updated accordingly, all green.

Thank you. The implementation looks much simpler now and the tests all
make sense to me and I can’t think of any other relevant “edge case”.

I have one more comment regarding the RFC text, which should result in
“minor” changes as per the policy of making clarifying changes:

  1. “Set in child before parent::__construct()” also fails, since the
    property slot is not yet initialized:

That explanation and example does not seem to be quite correct: It’s not
the $this->x = 'C'; assignment that fails, it’s the implicit CPP
assignment when calling parent::__construct(). The explanation should be
fixed and the // Error: Cannot modify readonly property P::$x comment
should be moved to the parent::__construct() call.

The test in the implementation was already correct.

Hi Tim,

Good catch thanks, wording updated!
The RFC is ready then. Any other comments from anyone else?

Nicolas

Hi

On 2/23/26 08:49, Nicolas Grekas wrote:

The RFC is ready then. Any other comments from anyone else?

Thank you. I don't have any further comments.

Best regards
Tim Düsterhus

On Mon, Feb 23, 2026, at 1:49 AM, Nicolas Grekas wrote:

Le dim. 22 févr. 2026 à 19:14, Tim Düsterhus <tim@bastelstu.be> a écrit :

Hi

On 2/19/26 10:49, Nicolas Grekas wrote:
> Thanks, I've added new test cases to cover this.
> I've also improved tests as suggested on the PR.
> And finally I updated the implementation to reuse IS_PROP_REINITABLE
> instead of adding new flags + use an approach that doesn't require walking
> the call stack.
> PR and RFC updated accordingly, all green.

Thank you. The implementation looks much simpler now and the tests all
make sense to me and I can't think of any other relevant “edge case”.

I have one more comment regarding the RFC text, which should result in
“minor” changes as per the policy of making clarifying changes:

1. “Set in child before parent::__construct()” also fails, since the
property slot is not yet initialized:

That explanation and example does not seem to be quite correct: It's not
the `$this->x = 'C';` assignment that fails, it's the implicit CPP
assignment when calling parent::__construct(). The explanation should be
fixed and the `// Error: Cannot modify readonly property P::$x` comment
should be moved to the `parent::__construct()` call.

The test in the implementation was already correct.

Hi Tim,

Good catch thanks, wording updated!
The RFC is ready then. Any other comments from anyone else?

Nicolas

Two minor non-substantive points.

1. The "supported operations" section all the way at the bottom seems redundant. That was already specified earlier.

2. It could be helpful to include an example of using property hooks in the intro; there's an example of the other 2 options, but not of hooks, which are also mentioned as a not-always-ideal alternative. (Let me know if you want help or suggestions with that example.)

Otherwise, I'm happy with where this ended up. Thanks!

--Larry Garfield

Le mar. 24 févr. 2026 à 20:06, Larry Garfield <larry@garfieldtech.com> a écrit :

On Mon, Feb 23, 2026, at 1:49 AM, Nicolas Grekas wrote:

Le dim. 22 févr. 2026 à 19:14, Tim Düsterhus <tim@bastelstu.be> a écrit :

Hi

On 2/19/26 10:49, Nicolas Grekas wrote:

Thanks, I’ve added new test cases to cover this.
I’ve also improved tests as suggested on the PR.
And finally I updated the implementation to reuse IS_PROP_REINITABLE
instead of adding new flags + use an approach that doesn’t require walking
the call stack.
PR and RFC updated accordingly, all green.

Thank you. The implementation looks much simpler now and the tests all
make sense to me and I can’t think of any other relevant “edge case”.

I have one more comment regarding the RFC text, which should result in
“minor” changes as per the policy of making clarifying changes:

  1. “Set in child before parent::__construct()” also fails, since the
    property slot is not yet initialized:

That explanation and example does not seem to be quite correct: It’s not
the $this->x = 'C'; assignment that fails, it’s the implicit CPP
assignment when calling parent::__construct(). The explanation should be
fixed and the // Error: Cannot modify readonly property P::$x comment
should be moved to the parent::__construct() call.

The test in the implementation was already correct.

Hi Tim,

Good catch thanks, wording updated!
The RFC is ready then. Any other comments from anyone else?

Nicolas

Two minor non-substantive points.

  1. The “supported operations” section all the way at the bottom seems redundant. That was already specified earlier.

  2. It could be helpful to include an example of using property hooks in the intro; there’s an example of the other 2 options, but not of hooks, which are also mentioned as a not-always-ideal alternative. (Let me know if you want help or suggestions with that example.)

Otherwise, I’m happy with where this ended up. Thanks!

Thanks for having a look!
Here is the update: https://wiki.php.net/rfc/promoted_readonly_constructor_reassign?do=diff&rev2%5B0%5D=1771797005&rev2%5B1%5D=1771961558&difftype=sidebyside

Cheers,
Nicolas

On Tue, Feb 24, 2026, at 1:34 PM, Nicolas Grekas wrote:

Otherwise, I'm happy with where this ended up. Thanks!

Thanks for having a look!
Here is the update:
PHP: rfc:promoted_readonly_constructor_reassign

Cheers,
Nicolas

Property hooks cannot be combined with CPP, so properties must be declared separately

This is untrue. The following code is legal:

class Point {
    public function __construct(
        public float $x = 0.0 { set => abs($value); },
        public float $y = 0.0 { set => abs($value); },
    ) {}
}

The point about readonly is valid, and worth keeping, as is the "affects all writes" question, but the code above is possible.

--Larry Garfield

Le mar. 24 févr. 2026 à 20:57, Larry Garfield <larry@garfieldtech.com> a écrit :

On Tue, Feb 24, 2026, at 1:34 PM, Nicolas Grekas wrote:

Otherwise, I’m happy with where this ended up. Thanks!

Thanks for having a look!
Here is the update:
https://wiki.php.net/rfc/promoted_readonly_constructor_reassign?do=diff&rev2%5B0%5D=1771797005&rev2%5B1%5D=1771961558&difftype=sidebyside

Cheers,
Nicolas

Property hooks cannot be combined with CPP, so properties must be declared separately

This is untrue. The following code is legal:

class Point {
public function __construct(
public float $x = 0.0 { set => abs($value); },
public float $y = 0.0 { set => abs($value); },
) {}
}

The point about readonly is valid, and worth keeping, as is the “affects all writes” question, but the code above is possible.

My bad, I should get used to them more :slight_smile:

https://wiki.php.net/rfc/promoted_readonly_constructor_reassign?do=diff&rev2%5B0%5D=1771961558&rev2%5B1%5D=1771972141&difftype=sidebyside

On Tue, Feb 24, 2026, at 4:29 PM, Nicolas Grekas wrote:

Le mar. 24 févr. 2026 à 20:57, Larry Garfield <larry@garfieldtech.com> a écrit :

On Tue, Feb 24, 2026, at 1:34 PM, Nicolas Grekas wrote:

>> Otherwise, I'm happy with where this ended up. Thanks!
>>
>
> Thanks for having a look!
> Here is the update:
> PHP: rfc:promoted_readonly_constructor_reassign
>
> Cheers,
> Nicolas

> Property hooks cannot be combined with CPP, so properties must be declared separately

This is untrue. The following code is legal:

class Point {
    public function __construct(
        public float $x = 0.0 { set => abs($value); },
        public float $y = 0.0 { set => abs($value); },
    ) {}
}

The point about readonly is valid, and worth keeping, as is the "affects all writes" question, but the code above is possible.

My bad, I should get used to them more :slight_smile:

PHP: rfc:promoted_readonly_constructor_reassign

Looks good now. Thanks. :slight_smile:

--Larry Garfield

Le jeu. 22 janv. 2026 à 16:33, Nicolas Grekas <nicolas.grekas+php@gmail.com> a écrit :

Dear all,

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

As a quick intro, my motivation for that RFC is that I find it quite annoying that readonly properties play badly with CPP (constructor property promotion).

Doing simple processing of any argument before assigning it to a readonly property forces opting out of CPP.

This RFC would allow setting once a readonly property in the body of a constructor after the property was previously (and implicitly) set using CPP.

This allows keeping property declarations in their compact form while still enabling validation, normalization, or conditional initialization.

Friendly reminder about this RFC. It’s been quiet for 13 days so I plan to start the vote on Thursday if there are no more concerns to the text of the proposal.

Cheers,
Nicolas