====== PHP RFC: Readonly Variables ====== * Version: 0.1 * Date: 2026-02-22 * Author: Joshua Rüsweg * Status: Under Discussion * Implementation: https://github.com/joshuaruesweg/php-src/pull/1 ===== 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. ===== 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. **Multiple Assignment:** Multiple variables can be declared as readonly in a single statement by separating them with a comma. **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()''. Readonly variables can also be declared inside closures: **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. **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. **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. 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. **Pass-by-reference is forbidden:** **Compound assignment operators are forbidden:** **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. **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. **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. **Readonly and static variables cannot be combined:** Declaring a variable as ''readonly'' that has previously been declared as ''static'' is forbidden, and vice versa. **''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. '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. **Destructuring assignment is not supported:** Using ''readonly'' with destructuring assignment is currently not supported and results in a parse error. ===== 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. * Yes * No * Abstain ===== 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: - Version merged into - Link to git commit(s) - 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.