Skip to content

Commit 7a3ef4e

Browse files
CPP-owning model
1 parent 67be986 commit 7a3ef4e

File tree

5 files changed

+45
-26
lines changed

5 files changed

+45
-26
lines changed

Zend/tests/readonly_props/cpp_reassign_child_redefine.phpt

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ $c1 = new C1();
2929
var_dump($c1->x);
3030

3131
// Case 2: Parent uses CPP and reassigns; child redefines as non-promoted.
32-
// Parent's CPP sets the initial value, but the reassignment fails because the
33-
// child's non-promoted redefinition does not open a reassignment window for the parent.
32+
// The child does not use CPP, so it does not claim CPP ownership of the property.
33+
// P2's CPP "owns" the reassignment window: P2's body write succeeds.
3434
class P2 {
3535
public function __construct(
3636
public readonly string $x = 'P1',
@@ -43,11 +43,7 @@ class C2 extends P2 {
4343
public readonly string $x;
4444

4545
public function __construct() {
46-
try {
47-
parent::__construct();
48-
} catch (Throwable $e) {
49-
echo get_class($e), ": ", $e->getMessage(), "\n";
50-
}
46+
parent::__construct();
5147
}
5248
}
5349

@@ -83,7 +79,6 @@ var_dump($c3->x);
8379
--EXPECT--
8480
Error: Cannot modify readonly property C1::$x
8581
string(1) "P"
86-
Error: Cannot modify readonly property C2::$x
87-
string(2) "P1"
82+
string(2) "P2"
8883
Error: Cannot modify readonly property C3::$x
8984
string(2) "C1"

Zend/tests/readonly_props/cpp_reassign_indirect_allowed.phpt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ class MultipleReassign {
2424
public function __construct(
2525
public readonly string $prop = 'default',
2626
) {
27-
$this->initProp();
27+
$this->initProp("first from method");
2828
try {
29-
$this->initProp(); // Second call - should fail
29+
$this->initProp("second from method"); // Second call - should fail
3030
} catch (Throwable $e) {
3131
echo get_class($e), ": ", $e->getMessage(), "\n";
3232
}
3333
}
3434

35-
private function initProp(): void {
36-
$this->prop = 'from method';
35+
private function initProp(string $v): void {
36+
$this->prop = $v;
3737
}
3838
}
3939

@@ -44,4 +44,4 @@ var_dump($mr->prop);
4444
--EXPECT--
4545
string(11) "from method"
4646
Error: Cannot modify readonly property MultipleReassign::$prop
47-
string(11) "from method"
47+
string(17) "first from method"

Zend/tests/readonly_props/cpp_reassign_reflection.phpt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Foo {
77
public function __construct(
88
public readonly string $bar = 'default',
99
) {
10-
$this->bar = 'overwritten in constructor';
10+
$this->bar = strtoupper($bar);
1111
}
1212
}
1313

@@ -31,9 +31,11 @@ try {
3131
} catch (Throwable $e) {
3232
echo get_class($e), ": ", $e->getMessage(), "\n";
3333
}
34+
var_dump($obj->bar);
3435

3536
?>
3637
--EXPECT--
3738
Error: Typed property Foo::$bar must not be accessed before initialization
38-
string(26) "overwritten in constructor"
39+
string(13) "EXPLICIT CALL"
3940
Error: Cannot modify readonly property Foo::$bar
41+
string(13) "EXPLICIT CALL"

Zend/tests/readonly_props/cpp_reassign_visibility.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ class Child2 extends Parent2 {
4949
$child2 = new Child2();
5050
var_dump($child2->prop);
5151

52-
// Case 3: public - child's init() CAN modify within CPP window
52+
// Case 3: public(set) - child's init() CAN modify within CPP window
5353
class Parent3 {
5454
public function __construct(
55-
public readonly string $prop = 'parent default',
55+
public public(set) readonly string $prop = 'parent default',
5656
) {
5757
$this->init();
5858
}

Zend/zend_object_handlers.c

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,16 +1111,38 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
11111111
variable_ptr = &EG(error_zval);
11121112
goto exit;
11131113
}
1114-
/* For promoted readonly properties being initialized for the first time,
1115-
* set IS_PROP_REINITABLE to allow one reassignment in the constructor.
1116-
* The flag will be cleared by zend_leave_helper when the constructor exits.
1117-
* We check the current execute data directly (no stack walk needed) because
1118-
* CPP initialization always runs within the constructor frame itself. */
1119-
if ((prop_info->flags & (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED)) == (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED)
1114+
/* For readonly properties initialized for the first time via CPP, set
1115+
* IS_PROP_REINITABLE to allow one reassignment in the constructor body.
1116+
* The flag is cleared by zend_leave_helper when the constructor exits.
1117+
*
1118+
* Classical case: the property is promoted in the declaring class and the
1119+
* executing constructor belongs to that class (scope == prop_info->ce).
1120+
*
1121+
* Extended case: a child class redeclared the property without CPP, so
1122+
* prop_info->ce is the child but the property isn't promoted there. CPP
1123+
* "ownership" still belongs to the ancestor whose constructor has CPP for
1124+
* this property name, so its body is allowed to reassign once. The clearing
1125+
* loop in zend_leave_helper iterates the exiting ctor's own promoted props,
1126+
* which share the same object slot, so cleanup happens automatically. */
1127+
bool reinitable = false;
1128+
if ((prop_info->flags & ZEND_ACC_READONLY)
11201129
&& (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT)
11211130
&& EG(current_execute_data)
1122-
&& (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_CTOR)
1123-
&& EG(current_execute_data)->func->common.scope == prop_info->ce) {
1131+
&& (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_CTOR)) {
1132+
zend_class_entry *ctor_scope = EG(current_execute_data)->func->common.scope;
1133+
if (prop_info->flags & ZEND_ACC_PROMOTED) {
1134+
reinitable = (ctor_scope == prop_info->ce);
1135+
} else if (ctor_scope != prop_info->ce) {
1136+
/* Child redeclared without CPP: check if the executing ctor's class
1137+
* has a CPP declaration for this property name. */
1138+
zend_property_info *scope_prop = zend_hash_find_ptr(
1139+
&ctor_scope->properties_info, prop_info->name);
1140+
reinitable = scope_prop != NULL
1141+
&& (scope_prop->flags & (ZEND_ACC_READONLY|ZEND_ACC_PROMOTED))
1142+
== (ZEND_ACC_READONLY|ZEND_ACC_PROMOTED);
1143+
}
1144+
}
1145+
if (reinitable) {
11241146
Z_PROP_FLAG_P(variable_ptr) = IS_PROP_REINITABLE;
11251147
} else {
11261148
Z_PROP_FLAG_P(variable_ptr) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);

0 commit comments

Comments
 (0)