Skip to content

Commit d88f16b

Browse files
committed
Allow readonly properties to be reinitialized once during cloning
1 parent db6840b commit d88f16b

11 files changed

+324
-17
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
Readonly property cannot be reset twice during cloning
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly int $bar
9+
) {}
10+
11+
public function __clone()
12+
{
13+
$this->bar = 2;
14+
var_dump($this);
15+
$this->bar = 3;
16+
}
17+
}
18+
19+
$foo = new Foo(1);
20+
21+
try {
22+
clone $foo;
23+
} catch (Error $exception) {
24+
echo $exception->getMessage() . "\n";
25+
}
26+
27+
echo "done";
28+
29+
?>
30+
--EXPECT--
31+
object(Foo)#2 (1) {
32+
["bar"]=>
33+
int(2)
34+
}
35+
Cannot modify readonly property Foo::$bar
36+
done
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Readonly property cannot be reset after cloning when there is no custom clone handler
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly int $bar,
9+
public readonly int $baz
10+
) {}
11+
12+
public function wrongCloneOld()
13+
{
14+
$instance = clone $this;
15+
$this->bar++;
16+
}
17+
18+
public function wrongCloneNew()
19+
{
20+
$instance = clone $this;
21+
$instance->baz++;
22+
}
23+
}
24+
25+
$foo = new Foo(1, 1);
26+
27+
try {
28+
$foo->wrongCloneOld();
29+
} catch (Error $exception) {
30+
echo $exception->getMessage() . "\n";
31+
}
32+
33+
try {
34+
$foo->wrongCloneNew();
35+
} catch (Error $exception) {
36+
echo $exception->getMessage() . "\n";
37+
}
38+
39+
?>
40+
--EXPECT--
41+
Cannot modify readonly property Foo::$bar
42+
Cannot modify readonly property Foo::$baz
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Readonly property cannot be reset after cloning when there is a custom clone handler
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly int $bar,
9+
public readonly int $baz
10+
) {}
11+
12+
public function __clone() {}
13+
14+
public function wrongCloneOld()
15+
{
16+
$instance = clone $this;
17+
$this->bar++;
18+
}
19+
20+
public function wrongCloneNew()
21+
{
22+
$instance = clone $this;
23+
$instance->baz++;
24+
}
25+
}
26+
27+
$foo = new Foo(1, 1);
28+
29+
try {
30+
$foo->wrongCloneOld();
31+
} catch (Error $exception) {
32+
echo $exception->getMessage() . "\n";
33+
}
34+
35+
try {
36+
$foo->wrongCloneNew();
37+
} catch (Error $exception) {
38+
echo $exception->getMessage() . "\n";
39+
}
40+
41+
?>
42+
--EXPECT--
43+
Cannot modify readonly property Foo::$bar
44+
Cannot modify readonly property Foo::$baz
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Readonly property can be reset once during cloning
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly int $bar
9+
) {}
10+
11+
public function __clone()
12+
{
13+
$this->bar++;
14+
}
15+
}
16+
17+
$foo = new Foo(1);
18+
19+
var_dump(clone $foo);
20+
21+
$foo2 = clone $foo;
22+
var_dump($foo2);
23+
24+
var_dump(clone $foo2);
25+
26+
?>
27+
--EXPECTF--
28+
object(Foo)#%d (%d) {
29+
["bar"]=>
30+
int(2)
31+
}
32+
object(Foo)#%d (%d) {
33+
["bar"]=>
34+
int(2)
35+
}
36+
object(Foo)#%d (%d) {
37+
["bar"]=>
38+
int(3)
39+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
--TEST--
2+
Test that __clone() can write to readonly properties
3+
--FILE--
4+
<?php
5+
6+
class Counter
7+
{
8+
private static int $counter = 0;
9+
10+
public readonly int $count;
11+
private readonly int $foo;
12+
13+
public function __construct()
14+
{
15+
$this->count = ++self::$counter;
16+
$this->foo = 0;
17+
}
18+
19+
public function count(?int $count = null): static
20+
{
21+
$new = clone $this;
22+
$new->count = $count ?? ++self::$counter;
23+
24+
return $new;
25+
}
26+
27+
public function __clone()
28+
{
29+
if (is_a(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? '', self::class, true)) {
30+
unset($this->count);
31+
} else {
32+
$this->count = ++self::$counter;
33+
}
34+
$this->foo = 1;
35+
}
36+
}
37+
38+
$a = new Counter();
39+
var_dump($a);
40+
41+
var_dump(clone $a);
42+
43+
$b = $a->count();
44+
var_dump($b);
45+
46+
$c = $a->count(123);
47+
var_dump($c);
48+
49+
?>
50+
--EXPECTF--
51+
object(Counter)#%d (2) {
52+
["count"]=>
53+
int(1)
54+
["foo":"Counter":private]=>
55+
int(0)
56+
}
57+
object(Counter)#%d (2) {
58+
["count"]=>
59+
int(2)
60+
["foo":"Counter":private]=>
61+
int(1)
62+
}
63+
object(Counter)#%d (2) {
64+
["count"]=>
65+
int(3)
66+
["foo":"Counter":private]=>
67+
int(1)
68+
}
69+
object(Counter)#%d (2) {
70+
["count"]=>
71+
int(123)
72+
["foo":"Counter":private]=>
73+
int(1)
74+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
Test that __clone() unset properties
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly stdClass $bar,
9+
) {}
10+
11+
public function __clone()
12+
{
13+
unset($this->bar);
14+
}
15+
}
16+
17+
$foo = new Foo(new stdClass());
18+
$foo2 = clone $foo;
19+
20+
var_dump($foo);
21+
var_dump($foo2);
22+
23+
?>
24+
--EXPECTF--
25+
object(Foo)#1 (%d) {
26+
["bar"]=>
27+
object(stdClass)#2 (%d) {
28+
}
29+
}
30+
object(Foo)#3 (%d) {
31+
["bar"]=>
32+
uninitialized(stdClass)
33+
}

Zend/zend_enum.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case
4343

4444
ZVAL_STR_COPY(OBJ_PROP_NUM(zobj, 0), case_name);
4545
if (backing_value_zv != NULL) {
46-
ZVAL_COPY(OBJ_PROP_NUM(zobj, 1), backing_value_zv);
46+
zval *prop = OBJ_PROP_NUM(zobj, 1);
47+
48+
ZVAL_COPY(prop, backing_value_zv);
49+
Z_PROP_FLAG_P(prop) = 0;
4750
}
4851

4952
return zobj;
@@ -179,7 +182,7 @@ void zend_enum_add_interfaces(zend_class_entry *ce)
179182

180183
if (ce->enum_backing_type != IS_UNDEF) {
181184
ce->interface_names[num_interfaces_before + 1].name = zend_string_copy(zend_ce_backed_enum->name);
182-
ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0);
185+
ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0);
183186
}
184187

185188
ce->default_object_handlers = &zend_enum_object_handlers;

Zend/zend_execute.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,8 +984,12 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf
984984
zval tmp;
985985

986986
if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
987-
zend_readonly_property_modification_error(info);
988-
return &EG(uninitialized_zval);
987+
if (Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE) {
988+
Z_PROP_FLAG_P(property_val) &= ~IS_PROP_REINITABLE;
989+
} else {
990+
zend_readonly_property_modification_error(info);
991+
return &EG(uninitialized_zval);
992+
}
989993
}
990994

991995
ZVAL_DEREF(value);
@@ -3125,6 +3129,9 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c
31253129
ZEND_ASSERT(type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET);
31263130
if (Z_TYPE_P(ptr) == IS_OBJECT) {
31273131
ZVAL_COPY(result, ptr);
3132+
} else if (Z_PROP_FLAG_P(ptr) & IS_PROP_REINITABLE) {
3133+
Z_PROP_FLAG_P(ptr) &= ~IS_PROP_REINITABLE;
3134+
ZVAL_COPY(result, ptr);
31283135
} else {
31293136
zend_readonly_property_modification_error(prop_info);
31303137
ZVAL_ERROR(result);

0 commit comments

Comments
 (0)