Skip to content

Commit 6f8fcf1

Browse files
Allow promoted readonly property to be reassigned once in constructor
1 parent 13b83a4 commit 6f8fcf1

30 files changed

Lines changed: 1071 additions & 22 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
Promoted readonly property reassignment - child can reassign after parent::__construct()
3+
--FILE--
4+
<?php
5+
6+
// When both parent and child have CPP for the same property, the child's
7+
// reassignment window must survive the parent constructor's exit cleanup.
8+
9+
class P {
10+
public function __construct(
11+
public readonly string $x = 'P_default',
12+
) {
13+
// Parent does NOT reassign
14+
}
15+
}
16+
17+
class C extends P {
18+
public function __construct(
19+
public readonly string $x = 'C_default',
20+
) {
21+
try {
22+
parent::__construct();
23+
} catch (Throwable $e) {
24+
echo get_class($e), ": ", $e->getMessage(), "\n";
25+
}
26+
// Child should still be able to reassign its own CPP property
27+
$this->x = 'C_reassigned';
28+
}
29+
}
30+
31+
$c = new C();
32+
var_dump($c->x);
33+
34+
// Also test with multiple instances
35+
$c2 = new C();
36+
var_dump($c2->x);
37+
38+
?>
39+
--EXPECT--
40+
Error: Cannot modify readonly property C::$x
41+
string(12) "C_reassigned"
42+
Error: Cannot modify readonly property C::$x
43+
string(12) "C_reassigned"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Promoted readonly property reassignment in constructor - basic
3+
--FILE--
4+
<?php
5+
6+
class Point {
7+
public function __construct(
8+
public readonly int $x = 0,
9+
public readonly int $y = 0,
10+
) {
11+
// Reassign promoted readonly properties - allowed once
12+
$this->x = abs($x);
13+
$this->y = abs($y);
14+
}
15+
}
16+
17+
$point = new Point();
18+
var_dump($point->x, $point->y);
19+
20+
$point2 = new Point(-5, -3);
21+
var_dump($point2->x, $point2->y);
22+
23+
?>
24+
--EXPECT--
25+
int(0)
26+
int(0)
27+
int(5)
28+
int(3)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
--TEST--
2+
Promoted readonly property reassignment in constructor - child cannot reassign parent's property
3+
--FILE--
4+
<?php
5+
6+
// Case 1: Parent does NOT use reassignment, child still cannot reassign
7+
class Parent1 {
8+
public function __construct(
9+
public readonly string $prop = 'parent default',
10+
) {
11+
// Parent does NOT reassign here
12+
}
13+
}
14+
15+
class Child1 extends Parent1 {
16+
public function __construct() {
17+
parent::__construct();
18+
// Child cannot reassign parent-owned promoted property
19+
try {
20+
$this->prop = 'child override';
21+
} catch (Throwable $e) {
22+
echo get_class($e), ": ", $e->getMessage(), "\n";
23+
}
24+
}
25+
}
26+
27+
$child = new Child1();
28+
var_dump($child->prop);
29+
30+
// Case 2: Parent USES reassignment, child cannot
31+
class Parent2 {
32+
public function __construct(
33+
public readonly string $prop = 'parent default',
34+
) {
35+
$this->prop = 'parent set'; // Uses the one reassignment
36+
}
37+
}
38+
39+
class Child2 extends Parent2 {
40+
public function __construct() {
41+
parent::__construct();
42+
// Child cannot reassign parent-owned promoted property
43+
try {
44+
$this->prop = 'child override';
45+
} catch (Throwable $e) {
46+
echo get_class($e), ": ", $e->getMessage(), "\n";
47+
}
48+
}
49+
}
50+
51+
$child2 = new Child2();
52+
var_dump($child2->prop);
53+
54+
// Case 3: Child with its own promoted property
55+
class Parent3 {
56+
public function __construct(
57+
public readonly string $parentProp = 'parent default',
58+
) {
59+
// Parent does NOT reassign here
60+
}
61+
}
62+
63+
class Child3 extends Parent3 {
64+
public function __construct(
65+
public readonly string $childProp = 'child default',
66+
) {
67+
parent::__construct();
68+
// Child cannot reassign parent's property, but can reassign its own
69+
try {
70+
$this->parentProp = 'child set parent';
71+
} catch (Throwable $e) {
72+
echo get_class($e), ": ", $e->getMessage(), "\n";
73+
}
74+
$this->childProp = 'child set own';
75+
}
76+
}
77+
78+
$child3 = new Child3();
79+
var_dump($child3->parentProp, $child3->childProp);
80+
81+
?>
82+
--EXPECT--
83+
Error: Cannot modify readonly property Parent1::$prop
84+
string(14) "parent default"
85+
Error: Cannot modify readonly property Parent2::$prop
86+
string(10) "parent set"
87+
Error: Cannot modify readonly property Parent3::$parentProp
88+
string(14) "parent default"
89+
string(13) "child set own"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Promoted readonly property reassignment in constructor - child preempt then parent ctor throws
3+
--FILE--
4+
<?php
5+
6+
class ParentCPP {
7+
public function __construct(
8+
public readonly string $prop = 'parent default',
9+
) {
10+
$this->prop = 'parent set';
11+
}
12+
}
13+
14+
class ChildCPP extends ParentCPP {
15+
public function __construct() {
16+
$this->prop = 'child set';
17+
try {
18+
parent::__construct();
19+
} catch (Throwable $e) {
20+
echo get_class($e), ": ", $e->getMessage(), "\n";
21+
}
22+
}
23+
}
24+
25+
$c = new ChildCPP();
26+
var_dump($c->prop);
27+
28+
?>
29+
--EXPECT--
30+
Error: Cannot modify readonly property ParentCPP::$prop
31+
string(9) "child set"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
--TEST--
2+
Promoted readonly property reassignment in constructor - child redefines parent property
3+
--FILE--
4+
<?php
5+
6+
// Case 1: Parent uses CPP, child redefines as non-promoted, child tries to reassign.
7+
// P1 owns the CPP reassignment window; it is cleared when P1's constructor exits,
8+
// before C1's body runs. So C1's write attempt fails.
9+
class P1 {
10+
public function __construct(
11+
public readonly string $x = 'P',
12+
) {}
13+
}
14+
15+
class C1 extends P1 {
16+
public readonly string $x;
17+
18+
public function __construct() {
19+
parent::__construct();
20+
try {
21+
$this->x = 'C';
22+
} catch (Throwable $e) {
23+
echo get_class($e), ": ", $e->getMessage(), "\n";
24+
}
25+
}
26+
}
27+
28+
$c1 = new C1();
29+
var_dump($c1->x);
30+
31+
// Case 2: Parent uses CPP and reassigns; child redefines as non-promoted.
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.
34+
class P2 {
35+
public function __construct(
36+
public readonly string $x = 'P1',
37+
) {
38+
$this->x = 'P2';
39+
}
40+
}
41+
42+
class C2 extends P2 {
43+
public readonly string $x;
44+
45+
public function __construct() {
46+
parent::__construct();
47+
}
48+
}
49+
50+
$c2 = new C2();
51+
var_dump($c2->x);
52+
53+
// Case 3: Parent uses CPP, child uses CPP redefinition.
54+
// Child's CPP opens the reassignment window for C3::$x. When parent::__construct()
55+
// runs, P3's CPP tries to initialize C3::$x again, which must fail since C3
56+
// owns the property and has already initialized it.
57+
class P3 {
58+
public function __construct(
59+
public readonly string $x = 'P',
60+
) {}
61+
}
62+
63+
class C3 extends P3 {
64+
public function __construct(
65+
public readonly string $x = 'C1',
66+
) {
67+
try {
68+
parent::__construct();
69+
} catch (Throwable $e) {
70+
echo get_class($e), ": ", $e->getMessage(), "\n";
71+
}
72+
}
73+
}
74+
75+
$c3 = new C3();
76+
var_dump($c3->x);
77+
78+
?>
79+
--EXPECT--
80+
Error: Cannot modify readonly property C1::$x
81+
string(1) "P"
82+
string(2) "P2"
83+
Error: Cannot modify readonly property C3::$x
84+
string(2) "C1"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Promoted readonly property reassignment in constructor - conditional initialization
3+
--FILE--
4+
<?php
5+
6+
class Config {
7+
public function __construct(
8+
public readonly ?string $cacheDir = null,
9+
) {
10+
$this->cacheDir ??= '/tmp/app_cache';
11+
}
12+
}
13+
14+
$config1 = new Config();
15+
var_dump($config1->cacheDir);
16+
17+
$config2 = new Config('/custom/cache');
18+
var_dump($config2->cacheDir);
19+
20+
?>
21+
--EXPECT--
22+
string(14) "/tmp/app_cache"
23+
string(13) "/custom/cache"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
Promoted readonly property reassignment in constructor - different object fails
3+
--FILE--
4+
<?php
5+
6+
// Constructor can modify its own promoted property, but not another object's
7+
class Foo {
8+
public function __construct(
9+
public readonly int $x = 0,
10+
?Foo $other = null,
11+
) {
12+
$this->x = $x * 2;
13+
if ($other !== null) {
14+
try {
15+
$other->x = 999;
16+
} catch (Throwable $e) {
17+
echo get_class($e), ": ", $e->getMessage(), "\n";
18+
}
19+
}
20+
}
21+
}
22+
23+
$a = new Foo(5);
24+
var_dump($a->x);
25+
26+
$b = new Foo(3, $a);
27+
var_dump($a->x, $b->x); // $a unchanged
28+
29+
?>
30+
--EXPECT--
31+
int(10)
32+
Error: Cannot modify readonly property Foo::$x
33+
int(10)
34+
int(6)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Promoted readonly properties cannot be reassigned when __construct() is called directly
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly string $value = 'default',
9+
) {
10+
$this->value = strtoupper($this->value);
11+
}
12+
}
13+
14+
$obj = new Foo('hello');
15+
var_dump($obj->value);
16+
17+
// Direct call fails: CPP assignment cannot reinitialize an already-set property
18+
try {
19+
$obj->__construct('world');
20+
} catch (Throwable $e) {
21+
echo get_class($e), ": ", $e->getMessage(), "\n";
22+
}
23+
var_dump($obj->value);
24+
25+
?>
26+
--EXPECT--
27+
string(5) "HELLO"
28+
Error: Cannot modify readonly property Foo::$value
29+
string(5) "HELLO"

0 commit comments

Comments
 (0)