PHP RFC: Allow Object Property Writes on Objects Referenced by Constants
- Version: 0.2
- Date: 2026-04-04
- Author: Khaled Alam, khaledalam.net@gmail.com
- Status: Draft
- Implementation: https://github.com/php/php-src/pull/20903
Introduction
This RFC allows writing to object properties (and `isset()` / `unset()` on properties) when the object instance is referenced by a constant.
PHP constants are immutable in the sense that the constant binding cannot be changed. However, objects in PHP are mutable by design: mutating an object property does not rebind the constant, it only updates the state of the referenced object.
Today, PHP rejects `CONST_OBJ->prop = ...` with a fatal error (“Cannot use temporary expression in write context”). This RFC proposes permitting property write operations in this specific scenario, aligning behavior with user expectations while keeping the constant binding immutable.
Proposal
This RFC proposes to allow property write operations where the base expression is a constant that evaluates to an object.
The change is intentionally narrow:
- It permits object property writes (`->`) and related operations (`isset`, `unset`, compound assignment,
increment/decrement, string concatenation) on objects referenced by constants.
- It permits passing constant object properties by reference to functions (including the `BP_VAR_FUNC_ARG`
path when the function is declared after the call site).
- It does not make constants mutable (rebinding remains illegal).
- It does not change array/dimension write semantics (`[]`), nor does it change “write-to-temporary” behavior for dims.
Specifically, the following operations become valid:
- Assignment to a property:
<?php const OBJ = new stdClass(); OBJ->prop = "value"; ?>
- Compound operations on a property:
<?php const OBJ = new stdClass(); OBJ->counter = 0; OBJ->counter++; OBJ->counter--; OBJ->counter += 10; OBJ->str = 'hello'; OBJ->str .= ' world'; ?>
- `isset()` / `unset()` on a property:
<?php const OBJ = new stdClass(); OBJ->prop = 1; var_dump(isset(OBJ->prop)); // true unset(OBJ->prop); var_dump(isset(OBJ->prop)); // false ?>
- Nested property operations:
<?php const OBJ = new stdClass(); OBJ->a = new stdClass(); OBJ->a->b = 7; var_dump(OBJ->a->b); // int(7) ?>
- Passing properties by reference:
<?php const OBJ = new stdClass(); OBJ->val = 10; function modify(&$v) { $v = 42; } modify(OBJ->val); var_dump(OBJ->val); // int(42) ?>
Semantics
- The constant binding remains immutable.
- The referenced object may be mutated, consistent with PHP's object behavior.
- If the constant does not evaluate to an object at runtime, existing behavior (type error/fatal in property access) applies as usual.
Note on Array Constants
A related but distinct scenario exists with array constants:
<?php const ARR = [1, 2, 3]; $ref = ARR; $ref[0] = 9; var_dump(ARR); // [1, 2, 3] — original unchanged ?>
Because arrays use copy-on-write semantics, modifying the copy has no effect on the original constant. Similarly:
<?php const C = [1, 2, 3]; function foo(): array { return C; } foo()[] = 4; var_dump(C); // [1, 2, 3] — unmodified, assignment is on a temporary ?>
These dim-write cases involve assignment to temporaries that silently have no effect. Whether such no-effect writes should produce a warning or error is a valid concern but is out of scope for this RFC and may be addressed separately (e.g. warning/throwing on `ASSIGN_DIM` when OP1 is not a pointer — i.e. not an indirect, reference, or object — as the operation can never have an effect). See the Future Scope section.
Non-goals / Explicitly Unchanged
This RFC does NOT change:
- Rebinding constants:
<?php const OBJ = new stdClass(); OBJ = new stdClass(); // still illegal (parse error) ?>
- Dimension/array writes or other write-to-temporary semantics:
<?php const ARR = [1,2,3]; ARR[0] = 9; // unchanged (still rejected as before) const OBJ = new stdClass(); OBJ['x'] = 1; // unchanged (still rejected as before) ?>
Implementation Details
The compiler change is in `zend_delayed_compile_prop()` in `Zend/zend_compile.c`. When the object AST node is a `ZEND_AST_CONST` and the access type is `BP_VAR_W`, `BP_VAR_RW`, `BP_VAR_UNSET`, or `BP_VAR_FUNC_ARG`, the compiler now emits a `ZEND_FETCH_CONSTANT` opcode that produces a `VAR` result (via `zend_emit_op`) instead of a `TMP` result (via `zend_emit_op_tmp`). This allows the subsequent property write opcodes to operate on the fetched object.
The `zend_compile_const()` function remains unchanged — constants used in read contexts (`BP_VAR_R`, `BP_VAR_IS`) still produce `TMP` results as before.
Why This Brings Value
This feature addresses a common surprise: PHP already permits objects in constants (since PHP 8.1), and developers naturally expect to mutate the object's state through the constant reference. The current fatal error is not about constant immutability per se, but rather about an overly broad “temporary write context” restriction in the compiler. Narrowly enabling property writes reduces friction, improves ergonomics, and matches the runtime model of objects without increasing language complexity for other write contexts.
Backward Incompatible Changes
None.
Code that previously terminated with a fatal error for `CONST_OBJ->prop = ...` will now execute successfully. This is a compatibility improvement — no previously-working code changes behavior.
Proposed PHP Version(s)
PHP 8.6
RFC Impact
To the Ecosystem
- IDEs / LSPs: Mostly positive; previously-invalid syntax becomes valid.
- Static analyzers: May need minor updates to remove a false-positive for property writes via constants. Semantics remain straightforward: constant binding is unchanged, object state may change.
- Formatters / linters: No expected changes beyond accepting the syntax.
To Existing Extensions
No expected impact. This is a core compilation change around property write access, not an extension API change.
To SAPIs
No expected impact.
Open Issues
None currently.
Future Scope
- No-effect dim writes: A future RFC could explore whether certain no-effect write cases should produce a warning or error. For example, `ASSIGN_DIM` when OP1 is not a “pointer” (indirect, reference, or object) can never have an effect, since the write is to a temporary that is immediately discarded. This is a distinct concern from the property-write issue addressed here.
- Other write-to-temporary semantics**: Cases like `foo()[0] = 1` where the return value is a temporary are similarly out of scope but could be revisited.
Voting Choices
Primary vote requiring a 2/3 majority to accept the RFC:
Patches and Tests
Proof of concept implementation:
Tests included in the PR:
- `Zend/tests/gh10497.phpt` — property assignment, compound updates, isset/unset, nested writes, by-ref passing
- `Zend/tests/gh10497_func_arg.phpt` — BP_VAR_FUNC_ARG path (forward-declared function with by-ref parameter)
- `Zend/tests/gh10497_guardrails.phpt` — array dim write on constant still rejected
- `Zend/tests/gh10497_guardrails_dim_obj.phpt` — dim write on constant object still rejected
- `Zend/tests/gh10497_guardrails_rebind.phpt` — constant rebinding still a parse error
- `Zend/tests/gh12102_3.phpt` — updated to reflect correct behavior for array constant by-ref passing
Implementation
(To be filled after merge)
- Merged into: PHP 8.6
- Git commits: ...
- Manual entry: ...
References
- PR #20903: https://github.com/php/php-src/pull/20903
- Internals discussion thread: https://news-web.php.net/php.internals/129157
Rejected Features
- Extending the change to dimension/array writes (out of scope for this RFC; involves distinct copy-on-write and temporary semantics)
- Changing “write-to-temporary” warning/error policy for dims (out of scope; may be addressed in a future RFC)
Changelog
- 2026-02-24: Initial draft created.
- 2026-04-04: v0.2 — Updated after PR review feedback. Added by-ref passing examples, implementation details section, note on array constant behavior per internals feedback, explicit test inventory, updated voting timeline.
Key changes from v0.1: - Added by-ref passing examples (confirmed working in tests) - Added implementation details section explaining the compiler change - Added “Note on Array Constants” section addressing Ilija's and Tim's feedback about silent no-op dim writes - Expanded Future Scope with Tim's suggestion about warning on ASSIGN_DIM with non-pointer OP1 - Removed “Abstain” from voting (PHP RFCs typically use Yes/No) - Updated vote close date to a future date - Targeted PHP 8.6 explicitly - Listed all test files in the Patches and Tests section - Removed Open Issues (the by-ref question is answered, isset/unset is confirmed) - Added full URLs to References - Added changelog entry for v0.2