====== PHP RFC: Allow Reassignment of Promoted Readonly Properties in Constructor ====== * Version: 0.1 * Date: 2026-01-21 * Author: Nicolas Grekas * Status: Draft * First Published at: https://wiki.php.net/rfc/promoted_readonly_constructor_reassign ===== Introduction ===== Readonly properties play badly with Constructor Property Promotion (CPP): doing simple processing of any argument before assigning it to a readonly property forces opting out of CPP. This RFC proposes allowing a single reassignment of promoted readonly properties within the constructor body. This allows keeping property declarations in their compact form while still enabling validation, normalization, or conditional initialization. ===== Problem Statement ===== PHP 8.1 introduced readonly properties, and PHP 8.0 introduced Constructor Property Promotion. While these features work well, there is a usability issue when combining them. Consider a class that needs to normalize input values: class Point { public function __construct( public readonly float $x = 0.0, public readonly float $y = 0.0, ) { // ERROR: Cannot modify readonly property Point::$x $this->x = abs($x); $this->y = abs($y); } } Currently, developers must choose between: **Option 1:** Abandon CPP and write verbose code: class Point { public readonly float $x; public readonly float $y; public function __construct(float $x = 0.0, float $y = 0.0) { $this->x = abs($x); $this->y = abs($y); } } **Option 2:** Use default parameter expressions (limited): class Point { public function __construct( public readonly float $x = 0.0, public readonly float $y = 0.0, ) { // Cannot use $x in default expression for $x } } **Option 3:** Use property hooks (PHP 8.4+), but these have different semantics and overhead. None of these options preserve the conciseness of CPP while allowing post-initialization logic. ===== Proposal ===== This RFC proposes that promoted readonly properties can be reassigned **exactly once** within the **constructor body** of the declaring class. ==== Rules ==== - The property must be declared using Constructor Property Promotion (have the ''ZEND_ACC_PROMOTED'' flag) - The property must be readonly - The reassignment must occur directly in the constructor body (not in methods called by the constructor, not in closures) - Only one reassignment is permitted; subsequent assignments will throw an Error - All other readonly semantics remain unchanged (no modification outside constructor, no unsetting, etc.) ==== Examples ==== === Basic Usage === class Point { public function __construct( public readonly float $x = 0.0, public readonly float $y = 0.0, ) { // Normalize values - allowed with this RFC $this->x = abs($x); $this->y = abs($y); } } $p = new Point(-5.0, -3.0); echo $p->x; // 5.0 echo $p->y; // 3.0 === Conditional Initialization === class Config { public function __construct( public readonly ?string $cacheDir = null, ) { $this->cacheDir ??= sys_get_temp_dir() . '/app_cache'; } } === Validation === class User { public function __construct( public readonly string $email, ) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException('Invalid email'); } $this->email = strtolower($email); // Normalize } } === Multiple Reassignments Fail === class Example { public function __construct( public readonly string $value = 'default', ) { $this->value = 'first'; // OK $this->value = 'second'; // Error: Cannot modify readonly property } } === Reassignment in Called Methods Fails === class Example { public function __construct( public readonly string $value = 'default', ) { $this->init(); // Reassignment inside init() will fail } private function init(): void { $this->value = 'modified'; // Error: Cannot modify readonly property } } === Non-Promoted Properties Unchanged === class Example { public readonly string $regular; public function __construct() { $this->regular = 'first'; $this->regular = 'second'; // Error: Cannot modify readonly property } } ===== Rationale ===== ==== Why Only Promoted Properties? ==== Constructor Property Promotion creates an implicit assignment at the very beginning of the constructor, before any user code runs. This is fundamentally different from regular properties where the developer controls when the first assignment happens. For non-promoted readonly properties, the developer already has full control: class Example { public readonly string $prop; public function __construct(string $value) { // Developer can do any processing before first assignment $processed = strtolower(trim($value)); $this->prop = $processed; // First and only assignment } } With CPP, this control is lost because the assignment happens automatically. ==== Why Only in Constructor Body? ==== Restricting reassignment to the constructor body (not called methods or closures) provides: - **Predictability**: The reassignment window is clearly defined - **Security**: Cannot accidentally expose reassignment capability - **Simplicity**: Easy to understand and audit - **Performance**: Simple runtime check (current function == constructor) ==== Relationship to __clone() ==== PHP already allows readonly property modification in ''%%__clone()%%'' via the ''IS_PROP_REINITABLE'' flag. This RFC extends the same mechanism to promoted properties in constructors, maintaining consistency in the engine. ===== Backward Incompatible Changes ===== None. This RFC only enables previously invalid code. All existing valid code continues to work identically. ===== Proposed PHP Version(s) ===== Next minor version (PHP 8.6 or later). ===== RFC Impact ===== ==== To SAPIs ==== None. ==== To Existing Extensions ==== None. Extensions that create objects with promoted readonly properties will see the same new behavior. ==== To Opcache ==== The JIT helpers delegate to ''zend_std_write_property'' for property initialization, so no JIT-specific changes are required. ===== Proposed Voting Choices ===== Primary vote: Accept this RFC for PHP 8.6? * Yes * No Requires 2/3 majority. ===== Patches and Tests ===== Implementation: https://github.com/php/php-src/pull/20996 ===== References ===== * [[https://wiki.php.net/rfc/readonly_properties_v2|Readonly Properties 2.0 RFC]] * [[https://wiki.php.net/rfc/constructor_promotion|Constructor Property Promotion RFC]] * [[https://wiki.php.net/rfc/readonly_amendments|Readonly Amendments RFC]] (clone-related changes)