diff --git a/UPGRADING b/UPGRADING index 467387a9ea3c..1cdf812b62cf 100644 --- a/UPGRADING +++ b/UPGRADING @@ -55,6 +55,11 @@ PHP 8.6 UPGRADE NOTES ======================================== - Core: + . It is now possible to write to object properties on objects stored in + constants. Previously this resulted in "Cannot use temporary expression + in write context". This change is limited to object property access + (->); dim/array writes on constants (e.g. CONST[0] = value) remain + a compile error. RFC: https://wiki.php.net/rfc/const_object_property_write . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. diff --git a/Zend/tests/gh10497.phpt b/Zend/tests/gh10497.phpt new file mode 100644 index 000000000000..7cbb308cb846 --- /dev/null +++ b/Zend/tests/gh10497.phpt @@ -0,0 +1,61 @@ +--TEST-- +GH-10497: Allow direct modification of object properties on constants +--FILE-- +prop = 123; +var_dump(OBJ->prop); + +OBJ->foo = 'bar'; +OBJ->baz = 456; +var_dump(OBJ->foo, OBJ->baz); + +OBJ->prop = 'overwritten'; +var_dump(OBJ->prop); + +OBJ->inner = new stdClass; +OBJ->inner->value = 999; +var_dump(OBJ->inner->value); + +OBJ->counter = 0; +OBJ->counter++; +OBJ->counter++; +OBJ->counter--; +var_dump(OBJ->counter); + +OBJ->str = 'hello'; +OBJ->str .= ' world'; +var_dump(OBJ->str); + +OBJ->temp = 'remove me'; +var_dump(isset(OBJ->temp)); +unset(OBJ->temp); +var_dump(isset(OBJ->temp)); + +var_dump(isset(OBJ->foo)); +var_dump(empty(OBJ->foo)); +var_dump(isset(OBJ->nonexistent)); +var_dump(empty(OBJ->nonexistent)); + +function incr(&$v) { $v++; } +OBJ->reftest = 10; +incr(OBJ->reftest); +var_dump(OBJ->reftest); + +?> +--EXPECT-- +int(123) +string(3) "bar" +int(456) +string(11) "overwritten" +int(999) +int(1) +string(11) "hello world" +bool(true) +bool(false) +bool(true) +bool(false) +bool(false) +bool(true) +int(11) diff --git a/Zend/tests/gh10497_func_arg.phpt b/Zend/tests/gh10497_func_arg.phpt new file mode 100644 index 000000000000..047cf4cfe45f --- /dev/null +++ b/Zend/tests/gh10497_func_arg.phpt @@ -0,0 +1,19 @@ +--TEST-- +GH-10497: Passing constant object property by reference via FUNC_ARG +--FILE-- +val = 10; +modify(OBJ->val); +var_dump(OBJ->val); + +function modify(&$v) { + $v = 42; +} + +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/gh10497_guardrails.phpt b/Zend/tests/gh10497_guardrails.phpt new file mode 100644 index 000000000000..a04b563b15e7 --- /dev/null +++ b/Zend/tests/gh10497_guardrails.phpt @@ -0,0 +1,9 @@ +--TEST-- +GH-10497: Guardrail - array dim write on constant still fails +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use temporary expression in write context in %s on line %d diff --git a/Zend/tests/gh10497_guardrails_dim_obj.phpt b/Zend/tests/gh10497_guardrails_dim_obj.phpt new file mode 100644 index 000000000000..c6ff2f63d565 --- /dev/null +++ b/Zend/tests/gh10497_guardrails_dim_obj.phpt @@ -0,0 +1,9 @@ +--TEST-- +GH-10497: Guardrail - dim write on constant object still fails +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use temporary expression in write context in %s on line %d diff --git a/Zend/tests/gh10497_guardrails_rebind.phpt b/Zend/tests/gh10497_guardrails_rebind.phpt new file mode 100644 index 000000000000..aa548b16d7d2 --- /dev/null +++ b/Zend/tests/gh10497_guardrails_rebind.phpt @@ -0,0 +1,9 @@ +--TEST-- +GH-10497: Guardrail - constant rebinding still fails +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected token "=" in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6734db09a2e9..b03c73804e2d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3179,6 +3179,28 @@ static zend_op *zend_delayed_compile_prop(znode *result, zend_ast *ast, uint32_t /* We will throw if $this doesn't exist, so there's no need to emit a JMP_NULL * check for a nullsafe access. */ + } else if (obj_ast->kind == ZEND_AST_CONST + && (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET + || type == BP_VAR_FUNC_ARG)) { + zend_ast *name_ast = obj_ast->child[0]; + bool is_fully_qualified; + zend_string *orig_name = zend_ast_get_str(name_ast); + zend_string *resolved_name = zend_resolve_const_name( + orig_name, name_ast->attr, &is_fully_qualified); + + opline = zend_emit_op(&obj_node, ZEND_FETCH_CONSTANT, NULL, NULL); + opline->op2_type = IS_CONST; + + if (is_fully_qualified || !FC(current_namespace)) { + opline->op1.num = 0; + opline->op2.constant = zend_add_const_name_literal( + resolved_name, false); + } else { + opline->op1.num = IS_CONSTANT_UNQUALIFIED_IN_NAMESPACE; + opline->op2.constant = zend_add_const_name_literal( + resolved_name, true); + } + opline->extended_value = zend_alloc_cache_slot(); } else { zend_short_circuiting_mark_inner(obj_ast); opline = zend_delayed_compile_var(&obj_node, obj_ast, type, false);