# RFC: Readonly Variables

- Status: Under discussion
- Authors: Joshua Rüsweg josh@php.net
- Date: 2026-02-22
- Version: 0.1
- Implementation: https://github.com/joshuaruesweg/php-src/pull/1
- Original: https://wiki.php.net/rfc/readonly-variables
- Page: https://externals.io/rfc/readonly-variables
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Implement Readonly Variables as outlined in the RFC? (closed)

Yes: 0 · No: 0 · Abstain: 0

## Discussions on the mailing list

- [\[RFC\] Readonly Variables](https://externals.io/message/130142.md) — started on 2026-02-23, 10 messages
- [\[RFC\] Readonly Variables](https://externals.io/message/130170.md) — started on 2026-02-25, 2 messages

---

## 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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?php
 
function modify(string &$value): void {}
 
readonly $name = "PHP";
modify($name); // Error: Cannot pass readonly variable by reference
?>
```

**Compound assignment operators are forbidden:**

```php
<?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
<?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
<?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
<?php
 
readonly $global_const = "global_value";
 
function test_global() {
    global $global_const; // Error: Cannot use global with readonly variable "global_const"
}
 
test_global();
?>
```

```php
<?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
<?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
<?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
<?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
<?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

- **Destructuring support** — `readonly [$a, $b] = [1, 2];` is not in scope for this RFC but could be added later.
- **`readonly` parameters** — marking function parameters as immutable could be a natural extension of this concept.
- **`readonly` in `foreach` loops** — `foreach ($items as readonly $item)`
- **Typed variables** — Typed variables, which are not immutable (`int $var = 1`).

## Voting Choices

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

**Vote: Implement Readonly Variables as outlined in the RFC?** — Yes: 0 · No: 0 · Abstain: 0 (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

- JavaScript `const` declaration: <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const>
- Swift `let` declaration: <https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics#Declaring-Constants-and-Variables>
- Discussion on the php.internals mailing list: <https://news-web.php.net/php.internals/130142>

## Rejected Features

None yet.

## Changelog

- 2026-02-22: Initial draft published.
- 2026-02-24: Clarified unset() behavior, added array and object examples.
