Skip to content

Fix GH-10497: Allow const obj->prop = value - #20903

Open
khaledalam wants to merge 5 commits into
php:masterfrom
khaledalam:fix-gh10497-allow-const-object-modification
Open

Fix GH-10497: Allow const obj->prop = value#20903
khaledalam wants to merge 5 commits into
php:masterfrom
khaledalam:fix-gh10497-allow-const-object-modification

Conversation

@khaledalam

@khaledalam khaledalam commented Jan 11, 2026

Copy link
Copy Markdown
Contributor

Description

RFC: https://wiki.php.net/rfc/const_object_property_write
Discussion thread: https://news-web.php.net/php.internals/129757
Voting thread: https://news-web.php.net/php.internals/132102

Fixes #10497 - Allows direct modification of object properties stored in constants, for both global constants and class constants.

Problem

Previously, this code would fail with a fatal error:

const a = new stdClass;
a->b = 42;  // Fatal error: Cannot use temporary expression in write context

The constant binding is immutable, but the object it holds is mutable by design — so the write should be allowed.

Solution

In zend_delayed_compile_prop(), when the object expression is a constant used in a write context (BP_VAR_W/RW/UNSET/FUNC_ARG), fetch the constant at runtime instead of treating it as a temporary:

  • ZEND_AST_CONST → emit ZEND_FETCH_CONSTANT (global constants)
  • ZEND_AST_CLASS_CONST → emit ZEND_FETCH_CLASS_CONSTANT (class constants)

Because the constant is fetched at runtime (not compile-time evaluated), the property write targets the real object.
The constant binding itself is never modified.

Scope

Supported:

  • CONST->prop = value; // global constant
  • Cls::CONST->prop = value; // class constant (via class name)
  • $obj::CONST->prop = value; // class constant (via instance)
  • …including compound assignment, ++/--, .=, isset()/unset(), by-reference passing, and nested chains.

Still rejected (unchanged):

  • Rebinding a constant (CONST = ...) — parse error
  • Dimension/array writes on constants (CONST[0] = ..., CONST["x"] = ...) , these write to a temporary, so they remain a compile error

Backward incompatible change

Enum cases are class constants, so they're affected. Writing to or unsetting a readonly case property, e.g. unset(Enum::Case->value) or Enum::Case->value = x , now raises the standard runtime readonly-property Error instead of the previous compile-time "Cannot use temporary expression in write context" fatal. This matches plain variable access ($x = Enum::Case; unset($x->value);). Enum case properties remain immutable.

@iluuu1994 iluuu1994 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @khaledalam! Looks reasonable overall.

Comment thread Zend/zend_compile.c Outdated
Comment thread Zend/zend_compile.c Outdated
Comment thread Zend/zend_compile.c Outdated
@khaledalam

Copy link
Copy Markdown
Contributor Author

Thanks @khaledalam! Looks reasonable overall.

Thanks @iluuu1994 for the feedback, all comments addressed.

@iluuu1994

Copy link
Copy Markdown
Member

Cool, thanks @khaledalam! This is a language change and should be discussed on the internals mailing list at least. I think this will impact GH-13800, but that's not a major issue.

@iluuu1994

Copy link
Copy Markdown
Member

Btw, one thing in particular people might object to is:

const arrayTest = [1, 2, 3];
arrayTest[] = 4;
var_dump(arrayTest);

This becomes valid, but prints:

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
}

I.e. the assignment has no effect, because it happens on a temporary value. Make sure to mention this in your e-mail to internals.

@khaledalam

Copy link
Copy Markdown
Contributor Author

I've opened an internals thread and sent an email for discussion: https://news-web.php.net/php.internals/129757

@khaledalam
khaledalam requested a review from iluuu1994 January 31, 2026 00:03
@iluuu1994

Copy link
Copy Markdown
Member

It sounds like @TimWolla objected, so this would need an RFC, or at least the mitigation from https://news-web.php.net/php.internals/129839.

@TimWolla TimWolla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my replies on the list were an objection against the PR as-is. Selecting request changes for visibility.

@khaledalam

Copy link
Copy Markdown
Contributor Author

Noted, I'll look into it

@khaledalam
khaledalam force-pushed the fix-gh10497-allow-const-object-modification branch from d488d10 to b0a2437 Compare April 3, 2026 23:52
@khaledalam
khaledalam force-pushed the fix-gh10497-allow-const-object-modification branch from b0a2437 to 2bd4eb9 Compare June 21, 2026 18:31
@TimWolla
TimWolla dismissed their stale review June 30, 2026 11:59

RFC was created.

@khaledalam
khaledalam requested a review from TimWolla August 9, 2026 04:21
@khaledalam

Copy link
Copy Markdown
Contributor Author

Voting closed. The RFC was accepted with 17 (Yes) to 2 (No) votes and 6 abstentions, which is 89.5% acceptance.

@khaledalam

Copy link
Copy Markdown
Contributor Author

@iluuu1994 @TimWolla code review please, RFC voting closed.

@iluuu1994

Copy link
Copy Markdown
Member

Didn't get to it today, tomorrow will have to do, hopefully before the beta 1 cutoff.

@iluuu1994

Copy link
Copy Markdown
Member

The implementation looks nothing like the one I reviewed. What's the reason this was changed?

@iluuu1994

Copy link
Copy Markdown
Member

I'm assuming the changes were made mainly to avoid making OBJ[0] = 1; legal. But it's a bit short-sighted:

OBJ[0]->prop = 42; // disallowed
OBJ->arr[0] = 42; // allowed
OBJ->arr[] = 42; // allowed

There's not much logic as to why the former is disallowed, but the latter is. What we really want to check is whether -> is contained anywhere in the chain.

@iluuu1994
iluuu1994 force-pushed the fix-gh10497-allow-const-object-modification branch from 873b39a to b92bc9f Compare August 11, 2026 20:48
@iluuu1994

Copy link
Copy Markdown
Member

Should be correct now. Feel free to check. I'll merge tomorrow.

@khaledalam

Copy link
Copy Markdown
Contributor Author

Should be correct now. Feel free to check. I'll merge tomorrow.

@iluuu1994 Heads up before you merge, I pushed a fix commit on top of your rewrite. It introduced a regression that I hit while testing the new implementation. Can you please re-review?

Compile-time evaluated constants in a write context produce an Invalid opcode fatal:

TRUE->prop = 1;                              // Fatal error: Invalid opcode 24/1/1.
NULL->prop = 1;                              // Fatal error: Invalid opcode 24/1/1.
PHP_EOL->prop = 1;                           // Fatal error: Invalid opcode 24/1/1.
class C { const N = 5; } C::N->prop = 1;     // Fatal error: Invalid opcode 24/1/1.
unset(C::N->prop);                           // Fatal error: Invalid opcode 76/1/1.
C::N->prop++;                                // Fatal error: Invalid opcode 132/1/1.
function f(&$v) {} f(C::N->prop);            // Fatal error: Invalid opcode 85/1/1.

On 8.5 these are all a clean Cannot use temporary expression in write context. Reproduces with and without opcache/JIT.

@khaledalam
khaledalam force-pushed the fix-gh10497-allow-const-object-modification branch from 78db6ef to c856726 Compare August 12, 2026 09:57
@khaledalam
khaledalam requested a review from dstogov as a code owner August 12, 2026 09:57
@iluuu1994

Copy link
Copy Markdown
Member

Thanks for the hint. I believe this was a pre-existing issue but didn't check. I tweaked the implementation a bit. Please check again.

khaledalam and others added 3 commits August 12, 2026 22:08
…nstants

Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
Use QM_ASSIGN instead of FETCH_CONSTANT, which avoids issues for
pseudo-constants like __COMPILER_HALT_OFFSET__ and special handling in the
optimizer. Also fix zend_optimizer_pass1() for ops that don't support op1=CONST.
@khaledalam
khaledalam force-pushed the fix-gh10497-allow-const-object-modification branch from 4110c60 to f593734 Compare August 13, 2026 02:56
@khaledalam

Copy link
Copy Markdown
Contributor Author

@iluuu1994 Thank you, It looks ok for me. I just rebased it to fix conflict.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot directly modify an object stored in a constant

3 participants