Skip to content

Commit 1667954

Browse files
Allow promoted readonly property to be reassigned once in constructor
1 parent 645e62b commit 1667954

File tree

2 files changed

+168
-1
lines changed

2 files changed

+168
-1
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
--TEST--
2+
Promoted readonly property can be reassigned once in constructor
3+
--FILE--
4+
<?php
5+
6+
class Point {
7+
public function __construct(
8+
public readonly float $x = 0.0,
9+
public readonly float $y = 0.0,
10+
) {
11+
$this->x = abs($x);
12+
$this->y = abs($y);
13+
}
14+
}
15+
16+
$point = new Point();
17+
var_dump($point->x, $point->y);
18+
19+
$point2 = new Point(-1.0, -2.0);
20+
var_dump($point2->x, $point2->y);
21+
22+
class FailingPoint {
23+
public function __construct(
24+
public readonly float $x = 0.0,
25+
) {
26+
$this->x = 10.0; // First reassignment - allowed
27+
try {
28+
$this->x = 20.0; // Second reassignment - should fail
29+
} catch (Error $e) {
30+
echo $e->getMessage(), "\n";
31+
}
32+
}
33+
}
34+
35+
$fp = new FailingPoint();
36+
var_dump($fp->x);
37+
38+
try {
39+
$point->x = 100.0;
40+
} catch (Error $e) {
41+
echo $e->getMessage(), "\n";
42+
}
43+
44+
class Foo {
45+
public function __construct(
46+
public readonly ?string $bar = null,
47+
) {
48+
$this->bar ??= 'modified in constructor';
49+
}
50+
}
51+
52+
$foo = new Foo();
53+
var_dump($foo->bar);
54+
55+
$foo2 = new Foo('passed value');
56+
var_dump($foo2->bar);
57+
58+
// Test that non-promoted readonly properties still cannot be reassigned
59+
class NonPromoted {
60+
public readonly string $prop;
61+
62+
public function __construct() {
63+
$this->prop = 'first';
64+
try {
65+
$this->prop = 'second'; // Should fail - not a promoted property
66+
} catch (Error $e) {
67+
echo $e->getMessage(), "\n";
68+
}
69+
}
70+
}
71+
72+
$np = new NonPromoted();
73+
var_dump($np->prop);
74+
75+
// Test mixed: promoted and non-promoted in same class
76+
class PromotedAndNonPromoted {
77+
public readonly string $nonPromoted;
78+
79+
public function __construct(
80+
public readonly string $promoted = 'default',
81+
) {
82+
$this->nonPromoted = 'first';
83+
$this->promoted = 'reassigned'; // Allowed (promoted)
84+
try {
85+
$this->nonPromoted = 'second'; // Should fail (non-promoted)
86+
} catch (Error $e) {
87+
echo $e->getMessage(), "\n";
88+
}
89+
}
90+
}
91+
92+
$m = new PromotedAndNonPromoted();
93+
var_dump($m->promoted, $m->nonPromoted);
94+
95+
// Reassignment is NOT allowed in methods called by the constructor
96+
class CalledMethod {
97+
public function __construct(
98+
public readonly string $prop = 'default',
99+
) {
100+
$this->initProp();
101+
}
102+
103+
private function initProp(): void {
104+
try {
105+
$this->prop = 'from method';
106+
} catch (Error $e) {
107+
echo $e->getMessage(), "\n";
108+
}
109+
}
110+
}
111+
112+
$cm = new CalledMethod();
113+
var_dump($cm->prop);
114+
115+
// Reassignment is NOT allowed in closures called by the constructor
116+
class ClosureInConstructor {
117+
public function __construct(
118+
public readonly string $prop = 'default',
119+
) {
120+
$fn = function() {
121+
try {
122+
$this->prop = 'from closure';
123+
} catch (Error $e) {
124+
echo $e->getMessage(), "\n";
125+
}
126+
};
127+
$fn();
128+
}
129+
}
130+
131+
$cc = new ClosureInConstructor();
132+
var_dump($cc->prop);
133+
134+
?>
135+
--EXPECT--
136+
float(0)
137+
float(0)
138+
float(1)
139+
float(2)
140+
Cannot modify readonly property FailingPoint::$x
141+
float(10)
142+
Cannot modify readonly property Point::$x
143+
string(23) "modified in constructor"
144+
string(12) "passed value"
145+
Cannot modify readonly property NonPromoted::$prop
146+
string(5) "first"
147+
Cannot modify readonly property PromotedAndNonPromoted::$nonPromoted
148+
string(10) "reassigned"
149+
string(5) "first"
150+
Cannot modify readonly property CalledMethod::$prop
151+
string(7) "default"
152+
Cannot modify readonly property ClosureInConstructor::$prop
153+
string(7) "default"

Zend/zend_object_handlers.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,21 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
11001100
variable_ptr = &EG(error_zval);
11011101
goto exit;
11021102
}
1103-
Z_PROP_FLAG_P(variable_ptr) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
1103+
/* For promoted readonly properties being initialized in the constructor,
1104+
* set IS_PROP_REINITABLE to allow one more assignment in the constructor body. */
1105+
if ((prop_info->flags & (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED)) == (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED)
1106+
&& (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT)) {
1107+
zend_execute_data *execute_data = EG(current_execute_data);
1108+
if (execute_data && EX(func)
1109+
&& ZEND_USER_CODE(EX(func)->common.type)
1110+
&& EX(func) == (zend_function *)zobj->ce->constructor) {
1111+
Z_PROP_FLAG_P(variable_ptr) = IS_PROP_REINITABLE;
1112+
} else {
1113+
Z_PROP_FLAG_P(variable_ptr) = 0;
1114+
}
1115+
} else {
1116+
Z_PROP_FLAG_P(variable_ptr) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
1117+
}
11041118
value = &tmp;
11051119
}
11061120

0 commit comments

Comments
 (0)