Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 float $x = 0.0,
public readonly float $y = 0.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.0, -3.0);
var_dump($point2->x, $point2->y);

?>
--EXPECT--
float(0)
float(0)
float(5)
float(3)
93 changes: 93 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,93 @@
--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 - leaves opportunity for child
}
}

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

$parent = new Parent1();
var_dump($parent->prop);

$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 already used the one reassignment
try {
$this->prop = 'child override';
} catch (Error $e) {
echo $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 doesn't reassign
}
}

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 (Error $e) {
echo $e->getMessage(), "\n";
}
$this->childProp = 'child set own';
}
}

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

?>
--EXPECT--
string(14) "parent default"
Cannot modify readonly property Parent1::$prop
string(14) "parent default"
Cannot modify readonly property Parent2::$prop
string(10) "parent set"
Cannot modify readonly property Parent3::$parentProp
string(14) "parent default"
string(13) "child set own"
60 changes: 60 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,60 @@
--TEST--
Promoted readonly property reassignment in constructor - child preempt then parent ctor throws
--FILE--
<?php

class ParentNoCPP {
public readonly string $prop;

public function __construct(
string $prop = 'parent default',
) {
try {
$this->prop = 'parent set';
} catch (Error) {
// readonly property set by child class
}
}
}

class ChildNoCPP extends ParentNoCPP {
public function __construct() {
$this->prop = 'child set';
parent::__construct();
}
}

class ParentCPP {
public function __construct(
public readonly string $prop = 'parent default',
) {
try {
$this->prop = 'parent set';
} catch (Error) {
// readonly property set by child class
}
}
}

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

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

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

?>
--EXPECT--
string(9) "child set"
Cannot modify readonly property ParentCPP::$prop
string(9) "child set"
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 ??= sys_get_temp_dir() . '/app_cache';
}
}

$config1 = new Config();
var_dump(str_contains($config1->cacheDir, 'app_cache'));

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

?>
--EXPECT--
bool(true)
string(13) "/custom/cache"
66 changes: 66 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,66 @@
--TEST--
Promoted readonly property reassignment in constructor - different object fails
--FILE--
<?php

class Point {
public function __construct(
public readonly float $x = 0.0,
) {
$this->x = abs($x);
}

public static function createFrom(Point $other): Point {
$new = new self();
// Cannot modify another object's readonly property
try {
$other->x = 999.0;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
return $new;
}
}

$p1 = new Point(-5.0);
var_dump($p1->x);

$p2 = Point::createFrom($p1);
var_dump($p1->x); // Unchanged

// Also test: constructor cannot modify another instance of the same class
class Counter {
private static ?Counter $last = null;

public function __construct(
public readonly int $value = 0,
) {
$this->value = $value + 1; // Allowed: own property

// Cannot modify previous instance
if (self::$last !== null) {
try {
self::$last->value = 999;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
self::$last = $this;
}
}

$c1 = new Counter(10);
var_dump($c1->value);

$c2 = new Counter(20);
var_dump($c1->value, $c2->value); // $c1 unchanged

?>
--EXPECT--
float(5)
Cannot modify readonly property Point::$x
float(5)
int(11)
Cannot modify readonly property Counter::$value
int(11)
int(21)
51 changes: 51 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,51 @@
--TEST--
Promoted readonly properties cannot be reassigned when __construct() is called directly
--FILE--
<?php

class Foo {
public function __construct(
public readonly string $value = 'default',
) {
// Don't use reassignment here, leave it available
}
}

$obj = new Foo('initial');
echo "Initial value: " . $obj->value . "\n";

// Direct call to __construct() should NOT allow reassignment
try {
$obj->__construct('modified');
echo "After direct __construct: " . $obj->value . "\n";
} catch (Error $e) {
echo "Error: " . $e->getMessage() . "\n";
}

// Also test with a class that uses reassignment
class Bar {
public function __construct(
public readonly string $value = 'default',
) {
$this->value = strtoupper($this->value);
}
}

$bar = new Bar('hello');
echo "Bar initial value: " . $bar->value . "\n";

// Direct call should fail during the CPP assignment (property not UNINIT)
// Note: The error happens inside the constructor because CPP assignment happens first
try {
$bar->__construct('world');
echo "After direct __construct: " . $bar->value . "\n";
} catch (Error $e) {
echo "Error: " . $e->getMessage() . "\n";
}

?>
--EXPECT--
Initial value: initial
Error: Cannot modify readonly property Foo::$value
Bar initial value: HELLO
Error: Cannot modify readonly property Bar::$value
63 changes: 63 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,63 @@
--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);

// Reassignment IS allowed in closures called by the constructor
class ClosureInConstructor {
public function __construct(
public readonly string $prop = 'default',
) {
$fn = function() {
$this->prop = 'from closure';
};
$fn();
}
}

$cc = new ClosureInConstructor();
var_dump($cc->prop);

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

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

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

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