Skip to content

Commit ea938d4

Browse files
committed
Add support for readonly classes
1 parent 9b19d90 commit ea938d4

25 files changed

+323
-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: 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: 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_compile.c

Lines changed: 13 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) {
@@ -7306,6 +7315,10 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
73067315
ZVAL_UNDEF(&value_zv);
73077316
}
73087317

7318+
if (!(flags & ZEND_ACC_READONLY) && (ce->ce_flags & ZEND_ACC_READONLY_CLASS)) {
7319+
flags |= ZEND_ACC_READONLY;
7320+
}
7321+
73097322
if (flags & ZEND_ACC_READONLY) {
73107323
if (!ZEND_TYPE_IS_SET(type)) {
73117324
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
}

ext/reflection/php_reflection.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ static void _class_string(smart_str *str, zend_class_entry *ce, zval *obj, char
348348
if (ce->ce_flags & ZEND_ACC_FINAL) {
349349
smart_str_append_printf(str, "final ");
350350
}
351+
if (ce->ce_flags & ZEND_ACC_READONLY_CLASS) {
352+
smart_str_append_printf(str, "readonly ");
353+
}
351354
smart_str_append_printf(str, "class ");
352355
}
353356
smart_str_append_printf(str, "%s", ZSTR_VAL(ce->name));
@@ -4863,6 +4866,12 @@ ZEND_METHOD(ReflectionClass, isFinal)
48634866
}
48644867
/* }}} */
48654868

4869+
/* Returns whether this class is readonly */
4870+
ZEND_METHOD(ReflectionClass, isReadonly)
4871+
{
4872+
_class_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_READONLY_CLASS);
4873+
}
4874+
48664875
/* {{{ Returns whether this class is abstract */
48674876
ZEND_METHOD(ReflectionClass, isAbstract)
48684877
{
@@ -4875,8 +4884,7 @@ ZEND_METHOD(ReflectionClass, getModifiers)
48754884
{
48764885
reflection_object *intern;
48774886
zend_class_entry *ce;
4878-
uint32_t keep_flags = ZEND_ACC_FINAL
4879-
| ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
4887+
uint32_t keep_flags = ZEND_ACC_FINAL | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_READONLY_CLASS;
48804888

48814889
if (zend_parse_parameters_none() == FAILURE) {
48824890
RETURN_THROWS();
@@ -7123,6 +7131,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */
71237131
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_IMPLICIT_ABSTRACT", ZEND_ACC_IMPLICIT_ABSTRACT_CLASS);
71247132
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_EXPLICIT_ABSTRACT", ZEND_ACC_EXPLICIT_ABSTRACT_CLASS);
71257133
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_FINAL", ZEND_ACC_FINAL);
7134+
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_READONLY", ZEND_ACC_READONLY_CLASS);
71267135

71277136
reflection_object_ptr = register_class_ReflectionObject(reflection_class_ptr);
71287137
reflection_object_ptr->create_object = reflection_objects_new;

ext/reflection/php_reflection.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ public function isAbstract(): bool {}
318318
/** @tentative-return-type */
319319
public function isFinal(): bool {}
320320

321+
public function isReadonly(): bool {}
322+
321323
/** @tentative-return-type */
322324
public function getModifiers(): int {}
323325

ext/reflection/php_reflection_arginfo.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: c9656b23db965e890e73d0064005b981ee1991cf */
2+
* Stub hash: eb8770218f10b3497e5293bc3c8b0173b1e2d262 */
33

44
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0)
55
ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0)
@@ -260,6 +260,8 @@ ZEND_END_ARG_INFO()
260260

261261
#define arginfo_class_ReflectionClass_isFinal arginfo_class_ReflectionFunctionAbstract_inNamespace
262262

263+
#define arginfo_class_ReflectionClass_isReadonly arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType
264+
263265
#define arginfo_class_ReflectionClass_getModifiers arginfo_class_ReflectionFunctionAbstract_getNumberOfParameters
264266

265267
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_isInstance, 0, 1, _IS_BOOL, 0)
@@ -697,6 +699,7 @@ ZEND_METHOD(ReflectionClass, isTrait);
697699
ZEND_METHOD(ReflectionClass, isEnum);
698700
ZEND_METHOD(ReflectionClass, isAbstract);
699701
ZEND_METHOD(ReflectionClass, isFinal);
702+
ZEND_METHOD(ReflectionClass, isReadonly);
700703
ZEND_METHOD(ReflectionClass, getModifiers);
701704
ZEND_METHOD(ReflectionClass, isInstance);
702705
ZEND_METHOD(ReflectionClass, newInstance);
@@ -963,6 +966,7 @@ static const zend_function_entry class_ReflectionClass_methods[] = {
963966
ZEND_ME(ReflectionClass, isEnum, arginfo_class_ReflectionClass_isEnum, ZEND_ACC_PUBLIC)
964967
ZEND_ME(ReflectionClass, isAbstract, arginfo_class_ReflectionClass_isAbstract, ZEND_ACC_PUBLIC)
965968
ZEND_ME(ReflectionClass, isFinal, arginfo_class_ReflectionClass_isFinal, ZEND_ACC_PUBLIC)
969+
ZEND_ME(ReflectionClass, isReadonly, arginfo_class_ReflectionClass_isReadonly, ZEND_ACC_PUBLIC)
966970
ZEND_ME(ReflectionClass, getModifiers, arginfo_class_ReflectionClass_getModifiers, ZEND_ACC_PUBLIC)
967971
ZEND_ME(ReflectionClass, isInstance, arginfo_class_ReflectionClass_isInstance, ZEND_ACC_PUBLIC)
968972
ZEND_ME(ReflectionClass, newInstance, arginfo_class_ReflectionClass_newInstance, ZEND_ACC_PUBLIC)

0 commit comments

Comments
 (0)