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)
83 changes: 83 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,83 @@
--TEST--
Promoted readonly property reassignment in constructor - child class can reassign parent's property
--FILE--
<?php

// Case 1: Parent does NOT use reassignment, child CAN 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 CAN reassign since parent didn't use the one reassignment
$this->prop = 'child override';
}
}

$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 can reassign both: parent's (not yet used) and its own
$this->parentProp = 'child set parent';
$this->childProp = 'child set own';
}
}

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

?>
--EXPECT--
string(14) "parent default"
string(14) "child override"
Cannot modify readonly property Parent2::$prop
string(10) "parent set"
string(16) "child set parent"
string(13) "child set own"
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)
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"
68 changes: 68 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_indirect_ops.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
--TEST--
Promoted readonly property reassignment in constructor - indirect operations (++, --, +=)
--FILE--
<?php

// Test that indirect operations also work for promoted readonly properties
// Note: each operation (++, --, +=, etc.) consumes the one allowed reassignment

class Counter {
public function __construct(
public readonly int $count = 0,
) {
// Single increment works
$this->count++;
}
}

$c = new Counter(5);
var_dump($c->count);

// Multiple operations count as reassignments - second fails
class MultiOp {
public function __construct(
public readonly int $value = 10,
) {
$this->value += 5; // First modification - allowed
try {
$this->value++; // Second modification - should fail
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
}

$m = new MultiOp();
var_dump($m->value);

// Decrement works too
class Decrement {
public function __construct(
public readonly int $value = 100,
) {
$this->value--;
}
}

$d = new Decrement();
var_dump($d->value);

// Assignment operators work
class AssignOps {
public function __construct(
public readonly string $text = 'hello',
) {
$this->text .= ' world';
}
}

$a = new AssignOps();
var_dump($a->text);

?>
--EXPECT--
int(6)
Cannot modify readonly property MultiOp::$value
int(15)
int(99)
string(11) "hello world"
25 changes: 25 additions & 0 deletions Zend/tests/readonly_props/cpp_reassign_multiple_fail.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
Promoted readonly property reassignment in constructor - multiple reassignments fail
--FILE--
<?php

class Example {
public function __construct(
public readonly string $value = 'default',
) {
$this->value = 'first'; // OK - first reassignment
try {
$this->value = 'second'; // Error - second reassignment
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
}

$ex = new Example();
var_dump($ex->value);

?>
--EXPECT--
Cannot modify readonly property Example::$value
string(5) "first"
Loading
Loading