Skip to content

Commit 3ccacde

Browse files
Allow reassign within nested calls
1 parent 06a4669 commit 3ccacde

File tree

2 files changed

+42
-22
lines changed

2 files changed

+42
-22
lines changed

Zend/tests/readonly_props/cpp_reassign_indirect_fail.phpt renamed to Zend/tests/readonly_props/cpp_reassign_indirect_allowed.phpt

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
--TEST--
2-
Promoted readonly property reassignment in constructor - indirect reassignment not allowed
2+
Promoted readonly property reassignment in constructor - indirect reassignment allowed
33
--FILE--
44
<?php
55

6-
// Reassignment is NOT allowed in methods called by the constructor
6+
// Reassignment IS allowed in methods called by the constructor
77
class CalledMethod {
88
public function __construct(
99
public readonly string $prop = 'default',
1010
) {
11-
try {
12-
$this->initProp();
13-
} catch (Error $e) {
14-
echo $e->getMessage(), "\n";
15-
}
11+
$this->initProp();
1612
}
1713

1814
private function initProp(): void {
@@ -23,28 +19,45 @@ class CalledMethod {
2319
$cm = new CalledMethod();
2420
var_dump($cm->prop);
2521

26-
// Reassignment is NOT allowed in closures called by the constructor
22+
// Reassignment IS allowed in closures called by the constructor
2723
class ClosureInConstructor {
2824
public function __construct(
2925
public readonly string $prop = 'default',
3026
) {
3127
$fn = function() {
3228
$this->prop = 'from closure';
3329
};
30+
$fn();
31+
}
32+
}
33+
34+
$cc = new ClosureInConstructor();
35+
var_dump($cc->prop);
36+
37+
// But second reassignment still fails
38+
class MultipleReassign {
39+
public function __construct(
40+
public readonly string $prop = 'default',
41+
) {
42+
$this->initProp();
3443
try {
35-
$fn();
44+
$this->initProp(); // Second call - should fail
3645
} catch (Error $e) {
3746
echo $e->getMessage(), "\n";
3847
}
3948
}
49+
50+
private function initProp(): void {
51+
$this->prop = 'from method';
52+
}
4053
}
4154

42-
$cc = new ClosureInConstructor();
43-
var_dump($cc->prop);
55+
$mr = new MultipleReassign();
56+
var_dump($mr->prop);
4457

4558
?>
4659
--EXPECT--
47-
Cannot modify readonly property CalledMethod::$prop
48-
string(7) "default"
49-
Cannot modify readonly property ClosureInConstructor::$prop
50-
string(7) "default"
60+
string(11) "from method"
61+
string(12) "from closure"
62+
Cannot modify readonly property MultipleReassign::$prop
63+
string(11) "from method"

Zend/zend_object_handlers.c

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,23 @@
4646
#define IN_ISSET ZEND_GUARD_PROPERTY_ISSET
4747
#define IN_HOOK ZEND_GUARD_PROPERTY_HOOK
4848

49-
/* Check if we're in the constructor of the class that declared the property.
50-
* Ensures that a child class cannot reassign a parent's promoted readonly property. */
49+
/* Check if we're within a constructor call chain for the class that declared the property.
50+
* Walks up the call stack to find if any frame is a constructor for zobj with the right scope.
51+
* This allows reassignment from methods/closures called by the constructor. */
5152
ZEND_API bool zend_is_in_property_declaring_class_constructor(const zend_property_info *prop_info, const zend_object *zobj)
5253
{
5354
zend_execute_data *ex = EG(current_execute_data);
54-
return ex && ex->func
55-
&& (ex->func->common.fn_flags & ZEND_ACC_CTOR)
56-
&& (ZEND_CALL_INFO(ex) & ZEND_CALL_HAS_THIS)
57-
&& Z_OBJ(ex->This) == zobj
58-
&& ex->func->common.scope == prop_info->ce;
55+
while (ex) {
56+
if (ex->func
57+
&& (ex->func->common.fn_flags & ZEND_ACC_CTOR)
58+
&& (ZEND_CALL_INFO(ex) & ZEND_CALL_HAS_THIS)
59+
&& Z_OBJ(ex->This) == zobj
60+
&& ex->func->common.scope == prop_info->ce) {
61+
return true;
62+
}
63+
ex = ex->prev_execute_data;
64+
}
65+
return false;
5966
}
6067

6168
static zend_arg_info zend_call_trampoline_arginfo[1] = {{0}};

0 commit comments

Comments
 (0)