Table of Contents

PHP RFC: Readonly Variables

Introduction

PHP has long supported immutable values through define() and the const keyword at the global and class scope. However, there is currently no way to declare a variable as immutable within a local or functional scope - a pattern that has proven valuable in languages such as JavaScript (const), Rust (let), and Swift (let).

This RFC proposes readonly variables: the ability to mark a variable as immutable after its initial assignment using the readonly modifier. Once declared, reassignment results in an error, making developer intent explicit and preventing a class of bugs that are otherwise undetectable without additional tooling.

<?php
 
readonly $connection = new PDO($dsn, $user, $password);
 
$connection = null; // Error: Cannot re-assign readonly variable
?>

Proposal

Immutable variables are declared by prefixing the variable name with the readonly keyword at the point of assignment. After the initial assignment, any attempt to reassign or pass the variable by reference results in an error.

<?php
 
readonly $dsn = 'mysql:host=localhost;dbname=myapp';
readonly $pdo = new PDO($dsn, 'root', '');
 
echo $dsn; // mysql:host=localhost;dbname=myapp
?>

Multiple Assignment:

Multiple variables can be declared as readonly in a single statement by separating them with a comma.

<?php
 
readonly $foo = "bar", $bar = "baz";
 
$foo = "Change-me!"; // Error: Cannot re-assign readonly variable
$bar = "Change-me!"; // Error: Cannot re-assign readonly variable
?>

Scoping:

Readonly variables follow the same scoping rules as regular variables, including function, method, and closure boundaries. They are not global constants and are not accessible via constant().

<?php
 
function getConnection(): PDO {
    readonly $pdo = new PDO('sqlite::memory:');
    // $pdo is readonly within this function scope only
    return $pdo;
}
 
$connection = getConnection();
$connection = null; // Valid reassignment of the variable.
?>

Readonly variables can also be declared inside closures:

<?php
 
$multiplier = 3;
 
$fn = function(int $value) use ($multiplier) {
    readonly $factor = 2;
    return $value * $factor * $multiplier;
};
 
echo $fn(5); // 30
?>

Scope and Conditional Initialization:

A readonly variable is only considered initialized, and therefore immutable, if its declaring branch was actually executed at runtime. If the declaration was never reached, the variable remains undefined and can be freely assigned.

<?php
 
if (true) {
    readonly $variable = "test";
}
 
if (false) {
    readonly $foo = "test";
}
 
$foo = "bar"; // Valid action, because variable is not initialized.
$variable = "Change-me!"; // Error: Cannot re-assign readonly variable
?>

Readonly arrays are fully immutable:

When an array is assigned to a readonly variable, the entire array is immutable. Neither the array itself nor its elements can be modified after the initial assignment.

<?php
 
readonly $arr = [1, 2, 3];
 
$arr[] = 4;    // Error: Cannot re-assign readonly variable
$arr[0] = 99;  // Error: Cannot re-assign readonly variable
?>

Readonly objects: the reference is immutable, not the internal state:

When an object is assigned to a readonly variable, the variable binding is immutable, meaning the object reference cannot be replaced with a different object. However, the internal state of the object itself can still be modified, consistent with how readonly behaves for class properties.

<?php
 
readonly $obj = new stdClass();
$obj->value = "hello"; // Valid: modifying the object's internal state is allowed
$obj->value = "world"; // Valid: modifying the object's internal state is allowed
 
$obj = new stdClass(); // Error: Cannot re-assign readonly variable
?>

Taking a reference is forbidden:

Taking a reference of a readonly variable is forbidden. This ensures that the immutability guarantee cannot be circumvented by modifying the variable indirectly through a reference. Additionally, declaring a readonly variable as a reference is a syntax error.

<?php
 
readonly $a = 1;
 
$b = &$a; // Error: Cannot take reference of readonly variable.
 
echo $a, PHP_EOL; // 1
var_dump(isset($b)); // bool(false)
?>

Pass-by-reference is forbidden:

<?php
 
function modify(string &$value): void {}
 
readonly $name = "PHP";
modify($name); // Error: Cannot pass readonly variable by reference
?>

Compound assignment operators are forbidden:

<?php
 
readonly $count = 1;
$count += 1; // Error: Cannot re-assign readonly variable
$count++;    // Error: Cannot re-assign readonly variable
 
readonly $string = "Hello";
$string .= " World!"; // Error: Cannot re-assign readonly variable
?>

Unset removes the readonly flag:

unset() behaves exactly as it does for regular variables: it removes the variable entirely. As a side effect, the readonly flag is also removed, allowing the variable name to be reused with a new assignment.

<?php
 
readonly $foo = "bar";
unset($foo);
var_dump(isset($foo)); // bool(false)
$foo = "test"; // Valid action, because unset removes the readonly flag.
?>

Readonly variables inside loops are limited to a single declaration:

Readonly variables are permitted inside loop bodies, but can only be declared once. Since loop variables are exposed to the outer scope after the loop completes, a readonly variable declared inside a loop cannot be re-initialized on subsequent iterations.

<?php
 
foreach ([1, 2, 3] as $item) {
    readonly $doubled = $item * 2; // Error on second iteration: Cannot re-assign readonly variable
}
?>

Global statement is forbidden:

Readonly variables declared in the global scope cannot be imported into a function scope via the global statement. Additionally, declaring a variable that has already been imported via global as readonly is forbidden, as global variables are internally references to the global symbol table.

<?php
 
readonly $global_const = "global_value";
 
function test_global() {
    global $global_const; // Error: Cannot use global with readonly variable "global_const"
}
 
test_global();
?>
<?php
 
$global_const = "global_value";
 
function test_global() {
    global $global_const;
 
    readonly $global_const = "inner_value"; // Error: Cannot declare reference variable as readonly.
}
 
test_global();
?>

Readonly and static variables cannot be combined:

Declaring a variable as readonly that has previously been declared as static is forbidden, and vice versa.

<?php
 
function test_static_then_readonly() {
    static $x = 0;
    readonly $x = 5; // Error: Cannot use readonly with static variable "x"
}
 
function test_readonly_then_static() {
    readonly $x = 5;
    static $x = 0; // Error: Cannot use static with readonly variable "x"
}
 
test_static_then_readonly();
test_readonly_then_static();
?>

extract() cannot overwrite readonly variables:

extract() is forbidden from overwriting readonly variables. If the extracted array contains a key that matches a readonly variable, the operation is aborted immediately with an error, leaving no variables from the array extracted at all.

<?php
 
readonly $foo = "bar";
readonly $baz = "qux";
 
$data = [
    'foo' => 'overwritten',
    'baz' => 'overwritten',
    'new_var' => 'hello',
];
 
extract($data); // Error: Cannot re-assign readonly variable.
 
echo $foo;                 // bar
echo $baz;                 // qux
var_dump(isset($new_var)); // bool(false)
?>

Variable variables cannot be used to re-assign readonly variables:

Attempting to re-assign a readonly variable through a variable variable is forbidden and results in an error at runtime.

<?php
 
readonly $foo = "bar";
$name = "foo";
 
$$name = "baz"; // Error: Cannot re-assign readonly variable
?>

Destructuring assignment is not supported:

Using readonly with destructuring assignment is currently not supported and results in a parse error.

<?php
 
readonly [$a, $b] = [1, 2]; // Parse error: syntax error, unexpected token "["
?>

Backward Incompatible Changes

This RFC introduces no breaking changes for existing code. The readonly keyword in the context of variable declarations is currently a parse error, meaning no valid existing PHP code uses this syntax.

Proposed PHP Version(s)

Next PHP 8.x

RFC Impact

To the Ecosystem

IDEs, language servers (e.g. Intelephense, PHPStan, Psalm), and static analyzers will need to be updated to recognize the readonly variable modifier and enforce immutability during static analysis. Auto-formatters and linters may also need updates to handle the new syntax. The benefit is that these tools can now guarantee immutability where previously they could only hint at it via docblock annotations such as @readonly on variables.

To Existing Extensions

Extensions that inspect or manipulate variable symbols (e.g. via zend_hash or custom opcache strategies) may need to account for the new Z_EXTRA_USER_READONLY_VAR flag which is stored in (zval).u2.extra. Extensions that do not interact with variable internals are unaffected. Additionally, there is readonly_var_flags in the zend_op_array.

To SAPIs

None.

Open Issues

None.

Future Scope

Voting Choices

This RFC requires a 2/3 majority to be accepted.

Implement Readonly Variables as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

A proof-of-concept implementation is available at: https://github.com/joshuaruesweg/php-src/pull/1

Implementation

To be filled after merging:

  1. Version merged into
  2. Link to git commit(s)
  3. Link to PHP manual entry

References

Rejected Features

None yet.

Changelog