Skip to content

Commit d6bbf1a

Browse files
committed
Allow readonly properties to be reinitialized once during cloning
Implements https://wiki.php.net/rfc/readonly_amendments
1 parent 735b94a commit d6bbf1a

15 files changed

+341
-20
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_API.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4221,6 +4221,10 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
42214221
ce->ce_flags |= ZEND_ACC_HAS_TYPE_HINTS;
42224222
}
42234223

4224+
if (access_type & ZEND_ACC_READONLY) {
4225+
ce->ce_flags |= ZEND_ACC_HAS_READONLY_PROPS;
4226+
}
4227+
42244228
if (ce->type == ZEND_INTERNAL_CLASS) {
42254229
property_info = pemalloc(sizeof(zend_property_info), 1);
42264230
} else {

Zend/zend_compile.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ typedef struct _zend_oparray_context {
240240
/* or IS_CONSTANT_VISITED_MARK | | | */
241241
#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */
242242
/* | | | */
243-
/* Class Flags (unused: 21,30,31) | | | */
243+
/* Class Flags (unused: 30,31) | | | */
244244
/* =========== | | | */
245245
/* | | | */
246246
/* Special class types | | | */
@@ -287,6 +287,8 @@ typedef struct _zend_oparray_context {
287287
/* | | | */
288288
/* Class is linked apart from variance obligations. | | | */
289289
#define ZEND_ACC_NEARLY_LINKED (1 << 20) /* X | | | */
290+
/* Class has readonly props | | | */
291+
#define ZEND_ACC_HAS_READONLY_PROPS (1 << 21) /* X | | | */
290292
/* | | | */
291293
/* stored in opcache (may be partially) | | | */
292294
#define ZEND_ACC_CACHED (1 << 22) /* X | | | */

Zend/zend_enum.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,17 @@ zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case
4141
zend_object *zobj = zend_objects_new(ce);
4242
ZVAL_OBJ(result, zobj);
4343

44-
ZVAL_STR_COPY(OBJ_PROP_NUM(zobj, 0), case_name);
44+
zval *zname = OBJ_PROP_NUM(zobj, 0);
45+
ZVAL_STR_COPY(zname, case_name);
46+
/* ZVAL_COPY does not set Z_PROP_FLAG, this needs to be cleared to avoid leaving IS_PROP_REINITABLE set */
47+
Z_PROP_FLAG_P(zname) = 0;
48+
4549
if (backing_value_zv != NULL) {
46-
ZVAL_COPY(OBJ_PROP_NUM(zobj, 1), backing_value_zv);
50+
zval *prop = OBJ_PROP_NUM(zobj, 1);
51+
52+
ZVAL_COPY(prop, backing_value_zv);
53+
/* ZVAL_COPY does not set Z_PROP_FLAG, this needs to be cleared to avoid leaving IS_PROP_REINITABLE set */
54+
Z_PROP_FLAG_P(prop) = 0;
4755
}
4856

4957
return zobj;
@@ -179,7 +187,7 @@ void zend_enum_add_interfaces(zend_class_entry *ce)
179187

180188
if (ce->enum_backing_type != IS_UNDEF) {
181189
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);
190+
ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0);
183191
}
184192

185193
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
@@ -1010,8 +1010,12 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf
10101010
zval tmp;
10111011

10121012
if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
1013-
zend_readonly_property_modification_error(info);
1014-
return &EG(uninitialized_zval);
1013+
if (Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE) {
1014+
Z_PROP_FLAG_P(property_val) &= ~IS_PROP_REINITABLE;
1015+
} else {
1016+
zend_readonly_property_modification_error(info);
1017+
return &EG(uninitialized_zval);
1018+
}
10151019
}
10161020

10171021
ZVAL_DEREF(value);
@@ -3161,6 +3165,9 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c
31613165
ZEND_ASSERT(type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET);
31623166
if (Z_TYPE_P(ptr) == IS_OBJECT) {
31633167
ZVAL_COPY(result, ptr);
3168+
} else if (Z_PROP_FLAG_P(ptr) & IS_PROP_REINITABLE) {
3169+
Z_PROP_FLAG_P(ptr) &= ~IS_PROP_REINITABLE;
3170+
ZVAL_COPY(result, ptr);
31643171
} else {
31653172
zend_readonly_property_modification_error(prop_info);
31663173
ZVAL_ERROR(result);

Zend/zend_execute.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ ZEND_API int ZEND_FASTCALL zend_handle_undef_args(zend_execute_data *call);
492492
} while (0)
493493

494494
#define ZEND_CLASS_HAS_TYPE_HINTS(ce) ((ce->ce_flags & ZEND_ACC_HAS_TYPE_HINTS) == ZEND_ACC_HAS_TYPE_HINTS)
495+
#define ZEND_CLASS_HAS_READONLY_PROPS(ce) ((ce->ce_flags & ZEND_ACC_HAS_READONLY_PROPS) == ZEND_ACC_HAS_READONLY_PROPS)
495496

496497
ZEND_API bool zend_verify_property_type(const zend_property_info *info, zval *property, bool strict);
497498
ZEND_COLD void zend_verify_property_type_error(const zend_property_info *info, const zval *property);

Zend/zend_inheritance.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1629,7 +1629,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
16291629
ce->ce_flags |= ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
16301630
}
16311631
}
1632-
ce->ce_flags |= parent_ce->ce_flags & (ZEND_HAS_STATIC_IN_METHODS | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_USE_GUARDS | ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES);
1632+
ce->ce_flags |= parent_ce->ce_flags & (ZEND_HAS_STATIC_IN_METHODS | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_READONLY_PROPS | ZEND_ACC_USE_GUARDS | ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES);
16331633
}
16341634
/* }}} */
16351635

0 commit comments

Comments
 (0)