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.
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.
This RFC proposes that promoted readonly properties can be reassigned exactly once within the constructor body of the declaring class.
ZEND_ACC_PROMOTED flag)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
class Config { public function __construct( public readonly ?string $cacheDir = null, ) { $this->cacheDir ??= sys_get_temp_dir() . '/app_cache'; } }
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 } }
class Example { public function __construct( public readonly string $value = 'default', ) { $this->value = 'first'; // OK $this->value = 'second'; // Error: Cannot modify readonly property } }
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 } }
class Example { public readonly string $regular; public function __construct() { $this->regular = 'first'; $this->regular = 'second'; // Error: Cannot modify readonly property } }
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.
Restricting reassignment to the constructor body (not called methods or closures) provides:
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.
None. This RFC only enables previously invalid code. All existing valid code continues to work identically.
Next minor version (PHP 8.6 or later).
None.
None. Extensions that create objects with promoted readonly properties will see the same new behavior.
The JIT helpers delegate to zend_std_write_property for property initialization, so no JIT-specific changes are required.
Primary vote: Accept this RFC for PHP 8.6?
Requires 2/3 majority.
Implementation: https://github.com/php/php-src/pull/20996