A PHP RFC under vote addresses the interaction between promoted readonly properties and constructor validation. Promotion assigns the property automatically at constructor entry. Because readonly normally prevents another assignment, a class that needs to normalize or validate the value must abandon promotion and declare the property manually.
Robert Landers' proposal permits exactly one reassignment of a promoted readonly property inside the constructor of the declaring class, through $this. Outside that narrow initialization window, readonly behavior remains unchanged. Asymmetric visibility continues to apply, and the implementation reuses the internal ISPROPREINITABLE flag.
This is an ergonomics repair rather than a broad relaxation of immutability. DTOs and value objects can keep concise promotion while replacing an input with its validated or normalized form. The vote was open through April 7 at the article date, so the syntax remained proposed rather than guaranteed.
One reassignment, only on the constructor chain
The proposed rule is narrow. The property must be readonly and promoted in the constructor (CPP). Reassignment is allowed while a constructor of the CPP-owning class is on the stack, including via a method or closure called from there. Once. Only on $this. private(set) and protected(set) still apply inside that chain.
Calling __construct() again on an already built object does not reopen the window. After the first construction finishes, the property is no longer uninitialized, and the readonly Error returns. ReflectionClass::newInstanceWithoutConstructor() followed by the first explicit __construct() still allows the reassignment, because that is the first construction. A second call fails.
A child does not inherit the parent's window. Ownership belongs to the class that promoted. If the child redeclares the same property without CPP, the parent can still reassign once. If the child also promotes, the child's CPP runs first and parent::__construct() trying to initialize again fails. Setting the property in the child before calling the parent fails for the same reason: the slot is no longer empty.
That covers the DTO that runs trim, strtolower, abs, or a default with ??. It does not cover multi-step initialization with two writes. The second write remains an Error. ++, --, +=, and .= count as the single reassignment.
When a manual declaration stays clearer
The RFC itself lists cases where dropping promotion is still the better shape. A nullable parameter that becomes a non-nullable property. PHPDoc that needs to live on the property (non-empty-string, lowercase-string). A long initialization with several derivations from the same input.
Property hooks in PHP 8.4 normalize on every assignment. Without extra private(set), they lose readonly's write-once guarantee. Different semantics, different cost. Replacing readonly with a hook only to keep CPP is a contract change. Useful when normalization should apply after construction. A poor fit when the object must be born immutable.
If the vote fails, the workaround stays the usual one: declare the property, process the parameter, assign once. Verbose. Readable. Already works on 8.1. Domain libraries that must run on 8.3 and 8.6 at once stay on that design even if the RFC passes, until the PHP floor rises.
There is a simple choice rule. If parameter and property share a type and the transformation fits in one line, the proposed reassignment cuts noise. If the type diverges or the transformation has branches, a traditional declaration documents the contract better than a second write hidden in the body.
Analyzers and team habit
Static tools and IDEs currently flag a second write to readonly as an error. If the RFC passes, they need to understand the window. Until then, enabling the syntax in CI on PHP 8.5 breaks the build. Psalm, PHPStan, and the editor parser do not vote on the RFC. They follow the runtime the pipeline uses.
Code review needs one question: is this write the promotion's normalization, or is someone using the constructor as a setter? The RFC does not want the second. A ++ on a promoted counter is easy to miss in a fast read and consumes the only permission.
__clone() could already reinitialize readonly via IS_PROP_REINITABLE. The proposal leaves that alone. Clone window and constructor window are distinct markers on purpose, so one does not leak into the other. Mixing clone, promotion, and reassignment on the same object needs a domain test, not an analogy with "readonly was already flexible in clone".
I would ask for one test per class that uses the new window: construct with dirty input, assert the normalized value, try a second write (Error), clone if the object is cloneable. Without that, the team only finds the limit when a factory calls the constructor twice.
Wait for the team's runtime, not the RFC text
The vote ran through 7 April and needed two thirds. Accepted text still needs the minor that lands the patch. The RFC points at PHP 8.6 or later. DTO libraries that adopt the syntax on merge day raise the application's floor. In a vendor shared between 8.4 and 8.6, the manual declaration remains the lowest common denominator.
Watching the vote makes sense if the team already feels the friction on value objects and already has a promotion pattern. Opening a broad refactor "because the RFC exists" spends the same effort as the manual declaration, with the extra risk that the analyzer and the pipeline runtime reject the code.
There is also the cost of teaching the rule. "Readonly does not change, except once in the constructor of the class that promoted, including a method called from there, never in a child that does not own the CPP" does not fit on a slide. It fits in an example in the internal guide, if the feature exists in the PHP you actually run.
Until the vote becomes a patch in the production image, the stable design stays the same: promotion when the value arrives ready, traditional declaration when the constructor has to work.
If your company is weighing something similar, reach me on LinkedIn or WhatsApp.