rfc:promoted_readonly_constructor_reassign

PHP RFC: Allow Reassignment of Promoted Readonly Properties in Constructor

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

  1. The property must be declared using Constructor Property Promotion (have the ZEND_ACC_PROMOTED flag)
  2. The property must be readonly
  3. The reassignment must occur directly in the constructor body (not in methods called by the constructor, not in closures)
  4. Only one reassignment is permitted; subsequent assignments will throw an Error
  5. 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:

  1. Predictability: The reassignment window is clearly defined
  2. Security: Cannot accidentally expose reassignment capability
  3. Simplicity: Easy to understand and audit
  4. 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

References

rfc/promoted_readonly_constructor_reassign.txt · Last modified: by nicolasgrekas