Skip to content

Commit 7850c10

Browse files
authored
Add support for readonly classes (#7305)
RFC: https://wiki.php.net/rfc/readonly_classes
1 parent 6beee1a commit 7850c10

29 files changed

+376
-7
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
The readonly class modifier can only be added once
3+
--FILE--
4+
<?php
5+
6+
readonly readonly class Foo
7+
{
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Multiple readonly modifiers are not allowed in %s on line %d
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Readonly classes cannot use dynamic properties
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
}
9+
10+
$foo = new Foo();
11+
12+
try {
13+
$foo->bar = 1;
14+
} catch (Error $exception) {
15+
echo $exception->getMessage() . "\n";
16+
}
17+
18+
?>
19+
--EXPECT--
20+
Cannot create dynamic property Foo::$bar
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
Readonly classes cannot apply the #[AllowDynamicProperties] attribute
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
readonly class Foo
8+
{
9+
}
10+
11+
?>
12+
--EXPECTF--
13+
Fatal error: Cannot apply #[AllowDynamicProperties] to readonly class Foo in %s on line %d
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
The readonly and final class modifiers can be defined in the same time
3+
--FILE--
4+
<?php
5+
6+
final readonly class Foo
7+
{
8+
}
9+
10+
readonly class Bar extends Foo
11+
{
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Fatal error: Class Bar cannot extend final class Foo in %s on line %d
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
Non-readonly class cannot extend a readonly class
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
}
9+
10+
class Bar extends Foo
11+
{
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Fatal error: Non-readonly class Bar cannot extend readonly class Foo in %s on line %d
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
Readonly class cannot extend a non-readonly class
3+
--FILE--
4+
<?php
5+
6+
class Foo
7+
{
8+
}
9+
10+
readonly class Bar extends Foo
11+
{
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Fatal error: Readonly class Bar cannot extend non-readonly class Foo in %s on line %d
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Readonly class can extend a readonly class
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
}
9+
10+
readonly class Bar extends Foo
11+
{
12+
}
13+
14+
?>
15+
--EXPECT--
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
Normal properties of a readonly class must have type
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
public $bar;
9+
}
10+
11+
?>
12+
--EXPECTF--
13+
Fatal error: Readonly property Foo::$bar must have type in %s on line %d
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Promoted properties of a readonly class must have type
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
public function __construct(
9+
private $bar
10+
) {}
11+
}
12+
13+
?>
14+
--EXPECTF--
15+
Fatal error: Readonly property Foo::$bar must have type in %s on line %d
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Normal properties of a readonly class are implicitly declared as readonly
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
public int $bar;
9+
10+
public function __construct() {
11+
$this->bar = 1;
12+
}
13+
}
14+
15+
$foo = new Foo();
16+
17+
try {
18+
$foo->bar = 2;
19+
} catch (Error $exception) {
20+
echo $exception->getMessage() . "\n";
21+
}
22+
23+
?>
24+
--EXPECT--
25+
Cannot modify readonly property Foo::$bar
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Promoted properties of a readonly class are implicitly declared as readonly
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
public function __construct(
9+
public int $bar
10+
) {}
11+
}
12+
13+
$foo = new Foo(1);
14+
15+
try {
16+
$foo->bar = 2;
17+
} catch (Error $exception) {
18+
echo $exception->getMessage() . "\n";
19+
}
20+
21+
?>
22+
--EXPECT--
23+
Cannot modify readonly property Foo::$bar
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
Declaring static property for a readonly class is forbidden
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo
7+
{
8+
public static int $bar;
9+
}
10+
11+
?>
12+
--EXPECTF--
13+
Fatal error: Readonly class Foo cannot declare static properties in %s on line %d
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Enums cannot be readonly
3+
--FILE--
4+
<?php
5+
6+
readonly enum Foo
7+
{
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Parse error: syntax error, unexpected token "enum", expecting "abstract" or "final" or "readonly" or "class" in %s on line %d
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Interfaces cannot be readonly
3+
--FILE--
4+
<?php
5+
6+
readonly interface Foo
7+
{
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Parse error: syntax error, unexpected token "interface", expecting "abstract" or "final" or "readonly" or "class" in %s on line %d
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Traits cannot be readonly
3+
--FILE--
4+
<?php
5+
6+
readonly trait Foo
7+
{
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Parse error: syntax error, unexpected token "trait", expecting "abstract" or "final" or "readonly" or "class" in %s on line %d

Zend/zend_ast.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
17151715
if (decl->flags & ZEND_ACC_FINAL) {
17161716
smart_str_appends(str, "final ");
17171717
}
1718+
if (decl->flags & ZEND_ACC_READONLY_CLASS) {
1719+
smart_str_appends(str, "readonly ");
1720+
}
17181721
smart_str_appends(str, "class ");
17191722
}
17201723
smart_str_appendl(str, ZSTR_VAL(decl->name), ZSTR_LEN(decl->name));

Zend/zend_attributes.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ static void validate_allow_dynamic_properties(
7171
if (scope->ce_flags & ZEND_ACC_INTERFACE) {
7272
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to interface");
7373
}
74+
if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) {
75+
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to readonly class %s",
76+
ZSTR_VAL(scope->name)
77+
);
78+
}
7479
scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES;
7580
}
7681

Zend/zend_compile.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,10 @@ uint32_t zend_add_class_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */
797797
zend_throw_exception(zend_ce_compile_error, "Multiple final modifiers are not allowed", 0);
798798
return 0;
799799
}
800+
if ((flags & ZEND_ACC_READONLY_CLASS) && (new_flag & ZEND_ACC_READONLY_CLASS)) {
801+
zend_throw_exception(zend_ce_compile_error, "Multiple readonly modifiers are not allowed", 0);
802+
return 0;
803+
}
800804
if ((new_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) && (new_flags & ZEND_ACC_FINAL)) {
801805
zend_throw_exception(zend_ce_compile_error,
802806
"Cannot use the final modifier on an abstract class", 0);
@@ -6673,6 +6677,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
66736677
if (property_flags) {
66746678
zend_op_array *op_array = CG(active_op_array);
66756679
zend_class_entry *scope = op_array->scope;
6680+
66766681
bool is_ctor =
66776682
scope && zend_is_constructor(op_array->function_name);
66786683
if (!is_ctor) {
@@ -6699,6 +6704,10 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
66996704
ZSTR_VAL(scope->name), ZSTR_VAL(name), ZSTR_VAL(str));
67006705
}
67016706

6707+
if (!(property_flags & ZEND_ACC_READONLY) && (scope->ce_flags & ZEND_ACC_READONLY_CLASS)) {
6708+
property_flags |= ZEND_ACC_READONLY;
6709+
}
6710+
67026711
/* Recompile the type, as it has different memory management requirements. */
67036712
zend_type type = ZEND_TYPE_INIT_NONE(0);
67046713
if (type_ast) {
@@ -7241,6 +7250,12 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
72417250
zend_error_noreturn(E_COMPILE_ERROR, "Properties cannot be declared abstract");
72427251
}
72437252

7253+
if ((ce->ce_flags & ZEND_ACC_READONLY_CLASS) && (flags & ZEND_ACC_STATIC)) {
7254+
zend_error_noreturn(E_COMPILE_ERROR, "Readonly class %s cannot declare static properties",
7255+
ZSTR_VAL(ce->name)
7256+
);
7257+
}
7258+
72447259
for (i = 0; i < children; ++i) {
72457260
zend_property_info *info;
72467261
zend_ast *prop_ast = list->child[i];
@@ -7306,6 +7321,10 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
73067321
ZVAL_UNDEF(&value_zv);
73077322
}
73087323

7324+
if ((ce->ce_flags & ZEND_ACC_READONLY_CLASS)) {
7325+
flags |= ZEND_ACC_READONLY;
7326+
}
7327+
73097328
if (flags & ZEND_ACC_READONLY) {
73107329
if (!ZEND_TYPE_IS_SET(type)) {
73117330
zend_error_noreturn(E_COMPILE_ERROR, "Readonly property %s::$%s must have type",

Zend/zend_compile.h

Lines changed: 4 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: 16,21,30,31) | | | */
243+
/* Class Flags (unused: 21,30,31) | | | */
244244
/* =========== | | | */
245245
/* | | | */
246246
/* Special class types | | | */
@@ -273,6 +273,9 @@ typedef struct _zend_oparray_context {
273273
/* without triggering a deprecation warning | | | */
274274
#define ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES (1 << 15) /* X | | | */
275275
/* | | | */
276+
/* Readonly class | | | */
277+
#define ZEND_ACC_READONLY_CLASS (1 << 16) /* X | | | */
278+
/* | | | */
276279
/* Parent class is resolved (CE). | | | */
277280
#define ZEND_ACC_RESOLVED_PARENT (1 << 17) /* X | | | */
278281
/* | | | */

Zend/zend_inheritance.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,13 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
14301430
}
14311431
}
14321432

1433+
if (UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) {
1434+
zend_error_noreturn(E_COMPILE_ERROR, "%s class %s cannot extend %s class %s",
1435+
ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "Readonly" : "Non-readonly", ZSTR_VAL(ce->name),
1436+
parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "readonly" : "non-readonly", ZSTR_VAL(parent_ce->name)
1437+
);
1438+
}
1439+
14331440
if (ce->parent_name) {
14341441
zend_string_release_ex(ce->parent_name, 0);
14351442
}

Zend/zend_language_parser.y

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ class_modifiers:
595595
class_modifier:
596596
T_ABSTRACT { $$ = ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; }
597597
| T_FINAL { $$ = ZEND_ACC_FINAL; }
598+
| T_READONLY { $$ = ZEND_ACC_READONLY_CLASS|ZEND_ACC_NO_DYNAMIC_PROPERTIES; }
598599
;
599600

600601
trait_declaration_statement:

build/gen_stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,10 @@ private function getFlagsAsString(): string
17971797
$flags[] = "ZEND_ACC_ABSTRACT";
17981798
}
17991799

1800+
if ($this->flags & Class_::MODIFIER_READONLY) {
1801+
$flags[] = "ZEND_ACC_READONLY_CLASS";
1802+
}
1803+
18001804
if ($this->isDeprecated) {
18011805
$flags[] = "ZEND_ACC_DEPRECATED";
18021806
}

0 commit comments

Comments
 (0)