Skip to content
28 changes: 28 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Promoted readonly property reassignment in constructor - basic
--FILE--
<?php

class Point {
public function __construct(
public readonly int $x = 0,
public readonly int $y = 0,
) {
// Reassign promoted readonly properties - allowed once
$this->x = abs($x);
$this->y = abs($y);
}
}

$point = new Point();
var_dump($point->x, $point->y);

$point2 = new Point(-5, -3);
var_dump($point2->x, $point2->y);

?>
--EXPECT--
int(0)
int(0)
int(5)
int(3)
89 changes: 89 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_child_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
--TEST--
Promoted readonly property reassignment in constructor - child cannot reassign parent's property
--FILE--
<?php

// Case 1: Parent does NOT use reassignment, child still cannot reassign
class Parent1 {
public function __construct(
public readonly string $prop = 'parent default',
) {
// Parent does NOT reassign here
}
}

class Child1 extends Parent1 {
public function __construct() {
parent::__construct();
// Child cannot reassign parent-owned promoted property
try {
$this->prop = 'child override';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$child = new Child1();
var_dump($child->prop);

// Case 2: Parent USES reassignment, child cannot
class Parent2 {
public function __construct(
public readonly string $prop = 'parent default',
) {
$this->prop = 'parent set'; // Uses the one reassignment
}
}

class Child2 extends Parent2 {
public function __construct() {
parent::__construct();
// Child cannot reassign parent-owned promoted property
try {
$this->prop = 'child override';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$child2 = new Child2();
var_dump($child2->prop);

// Case 3: Child with its own promoted property
class Parent3 {
public function __construct(
public readonly string $parentProp = 'parent default',
) {
// Parent does NOT reassign here
}
}

class Child3 extends Parent3 {
public function __construct(
public readonly string $childProp = 'child default',
) {
parent::__construct();
// Child cannot reassign parent's property, but can reassign its own
try {
$this->parentProp = 'child set parent';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
$this->childProp = 'child set own';
}
}

$child3 = new Child3();
var_dump($child3->parentProp, $child3->childProp);

?>
--EXPECT--
Error: Cannot modify readonly property Parent1::$prop
string(14) "parent default"
Error: Cannot modify readonly property Parent2::$prop
string(10) "parent set"
Error: Cannot modify readonly property Parent3::$parentProp
string(14) "parent default"
string(13) "child set own"
31 changes: 31 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_child_preempt_parent.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Promoted readonly property reassignment in constructor - child preempt then parent ctor throws
--FILE--
<?php

class ParentCPP {
public function __construct(
public readonly string $prop = 'parent default',
) {
$this->prop = 'parent set';
}
}

class ChildCPP extends ParentCPP {
public function __construct() {
$this->prop = 'child set';
try {
parent::__construct();
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$c = new ChildCPP();
var_dump($c->prop);

?>
--EXPECT--
Error: Cannot modify readonly property ParentCPP::$prop
string(9) "child set"
84 changes: 84 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_child_redefine.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
--TEST--
Promoted readonly property reassignment in constructor - child redefines parent property
--FILE--
<?php

// Case 1: Parent uses CPP, child redefines as non-promoted, child tries to reassign.
// P1 owns the CPP reassignment window; it is cleared when P1's constructor exits,
// before C1's body runs. So C1's write attempt fails.
class P1 {
public function __construct(
public readonly string $x = 'P',
) {}
}

class C1 extends P1 {
public readonly string $x;

public function __construct() {
parent::__construct();
try {
$this->x = 'C';
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$c1 = new C1();
var_dump($c1->x);

// Case 2: Parent uses CPP and reassigns; child redefines as non-promoted.
// The child does not use CPP, so it does not claim CPP ownership of the property.
// P2's CPP "owns" the reassignment window: P2's body write succeeds.
class P2 {
public function __construct(
public readonly string $x = 'P1',
) {
$this->x = 'P2';
}
}

class C2 extends P2 {
public readonly string $x;

public function __construct() {
parent::__construct();
}
}

$c2 = new C2();
var_dump($c2->x);

// Case 3: Parent uses CPP, child uses CPP redefinition.
// Child's CPP opens the reassignment window for C3::$x. When parent::__construct()
// runs, P3's CPP tries to initialize C3::$x again, which must fail since C3
// owns the property and has already initialized it.
class P3 {
public function __construct(
public readonly string $x = 'P',
) {}
}

class C3 extends P3 {
public function __construct(
public readonly string $x = 'C1',
) {
try {
parent::__construct();
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}

$c3 = new C3();
var_dump($c3->x);

?>
--EXPECT--
Error: Cannot modify readonly property C1::$x
string(1) "P"
string(2) "P2"
Error: Cannot modify readonly property C3::$x
string(2) "C1"
23 changes: 23 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_conditional.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Promoted readonly property reassignment in constructor - conditional initialization
--FILE--
<?php

class Config {
public function __construct(
public readonly ?string $cacheDir = null,
) {
$this->cacheDir ??= '/tmp/app_cache';
}
}

$config1 = new Config();
var_dump($config1->cacheDir);

$config2 = new Config('/custom/cache');
var_dump($config2->cacheDir);

?>
--EXPECT--
string(14) "/tmp/app_cache"
string(13) "/custom/cache"
34 changes: 34 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_different_object.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
Promoted readonly property reassignment in constructor - different object fails
--FILE--
<?php

// Constructor can modify its own promoted property, but not another object's
class Foo {
public function __construct(
public readonly int $x = 0,
?Foo $other = null,
) {
$this->x = $x * 2;
if ($other !== null) {
try {
$other->x = 999;
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}
}
}

$a = new Foo(5);
var_dump($a->x);

$b = new Foo(3, $a);
var_dump($a->x, $b->x); // $a unchanged

?>
--EXPECT--
int(10)
Error: Cannot modify readonly property Foo::$x
int(10)
int(6)
29 changes: 29 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_direct_ctor_call.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Promoted readonly properties cannot be reassigned when __construct() is called directly
--FILE--
<?php

class Foo {
public function __construct(
public readonly string $value = 'default',
) {
$this->value = strtoupper($this->value);
}
}

$obj = new Foo('hello');
var_dump($obj->value);

// Direct call fails: CPP assignment cannot reinitialize an already-set property
try {
$obj->__construct('world');
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
var_dump($obj->value);

?>
--EXPECT--
string(5) "HELLO"
Error: Cannot modify readonly property Foo::$value
string(5) "HELLO"
47 changes: 47 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_indirect_allowed.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--TEST--
Promoted readonly property reassignment in constructor - indirect reassignment allowed
--FILE--
<?php

// Reassignment IS allowed in methods called by the constructor
class CalledMethod {
public function __construct(
public readonly string $prop = 'default',
) {
$this->initProp();
}

private function initProp(): void {
$this->prop = 'from method';
}
}

$cm = new CalledMethod();
var_dump($cm->prop);

// But second reassignment still fails
class MultipleReassign {
public function __construct(
public readonly string $prop = 'default',
) {
$this->initProp("first from method");
try {
$this->initProp("second from method"); // Second call - should fail
} catch (Throwable $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
}

private function initProp(string $v): void {
$this->prop = $v;
}
}

$mr = new MultipleReassign();
var_dump($mr->prop);

?>
--EXPECT--
string(11) "from method"
Error: Cannot modify readonly property MultipleReassign::$prop
string(17) "first from method"
Loading
Loading