Skip to content

Commit 995c785

Browse files
committed
Add support for readonly classes
1 parent 90c16db commit 995c785

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
@@ -1680,6 +1680,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
16801680
if (decl->flags & ZEND_ACC_FINAL) {
16811681
smart_str_appends(str, "final ");
16821682
}
1683+
if (decl->flags & ZEND_ACC_READONLY_CLASS) {
1684+
smart_str_appends(str, "readonly ");
1685+
}
16831686
smart_str_appends(str, "class ");
16841687
}
16851688
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
@@ -799,6 +799,10 @@ uint32_t zend_add_class_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */
799799
zend_throw_exception(zend_ce_compile_error, "Multiple final modifiers are not allowed", 0);
800800
return 0;
801801
}
802+
if ((flags & ZEND_ACC_READONLY_CLASS) && (new_flag & ZEND_ACC_READONLY_CLASS)) {
803+
zend_throw_exception(zend_ce_compile_error, "Multiple readonly modifiers are not allowed", 0);
804+
return 0;
805+
}
802806
if ((new_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) && (new_flags & ZEND_ACC_FINAL)) {
803807
zend_throw_exception(zend_ce_compile_error,
804808
"Cannot use the final modifier on an abstract class", 0);
@@ -6654,6 +6658,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
66546658
if (property_flags) {
66556659
zend_op_array *op_array = CG(active_op_array);
66566660
zend_class_entry *scope = op_array->scope;
6661+
66576662
bool is_ctor =
66586663
scope && zend_is_constructor(op_array->function_name);
66596664
if (!is_ctor) {
@@ -6680,6 +6685,10 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
66806685
ZSTR_VAL(scope->name), ZSTR_VAL(name), ZSTR_VAL(str));
66816686
}
66826687

6688+
if (!(property_flags & ZEND_ACC_READONLY) && (scope->ce_flags & ZEND_ACC_READONLY_CLASS)) {
6689+
property_flags |= ZEND_ACC_READONLY;
6690+
}
6691+
66836692
/* Recompile the type, as it has different memory management requirements. */
66846693
zend_type type = ZEND_TYPE_INIT_NONE(0);
66856694
if (type_ast) {
@@ -7279,6 +7288,10 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
72797288
ZVAL_UNDEF(&value_zv);
72807289
}
72817290

7291+
if (!(flags & ZEND_ACC_READONLY) && (ce->ce_flags & ZEND_ACC_READONLY_CLASS)) {
7292+
flags |= ZEND_ACC_READONLY;
7293+
}
7294+
72827295
if (flags & ZEND_ACC_READONLY) {
72837296
if (!ZEND_TYPE_IS_SET(type)) {
72847297
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: 15,16,21,30,31) | | | */
243+
/* Class Flags (unused: 16,21,30,31) | | | */
244244
/* =========== | | | */
245245
/* | | | */
246246
/* Special class types | | | */
@@ -269,6 +269,9 @@ typedef struct _zend_oparray_context {
269269
/* User class has methods with static variables | | | */
270270
#define ZEND_HAS_STATIC_IN_METHODS (1 << 14) /* X | | | */
271271
/* | | | */
272+
/* Readonly class | | | */
273+
#define ZEND_ACC_READONLY_CLASS (1 << 15) /* X | | | */
274+
/* | | | */
272275
/* Parent class is resolved (CE). | | | */
273276
#define ZEND_ACC_RESOLVED_PARENT (1 << 17) /* X | | | */
274277
/* | | | */

Zend/zend_inheritance.c

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

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

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
@@ -1774,6 +1774,10 @@ private function getFlagsAsString(): string
17741774
$flags[] = "ZEND_ACC_ABSTRACT";
17751775
}
17761776

1777+
if ($this->flags & Class_::MODIFIER_READONLY) {
1778+
$flags[] = "ZEND_ACC_READONLY_CLASS";
1779+
}
1780+
17771781
if ($this->isDeprecated) {
17781782
$flags[] = "ZEND_ACC_DEPRECATED";
17791783
}

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));
@@ -4827,6 +4830,12 @@ ZEND_METHOD(ReflectionClass, isFinal)
48274830
}
48284831
/* }}} */
48294832

4833+
/* Returns whether this class is readonly */
4834+
ZEND_METHOD(ReflectionClass, isReadonly)
4835+
{
4836+
_class_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_READONLY_CLASS);
4837+
}
4838+
48304839
/* {{{ Returns whether this class is abstract */
48314840
ZEND_METHOD(ReflectionClass, isAbstract)
48324841
{
@@ -4839,8 +4848,7 @@ ZEND_METHOD(ReflectionClass, getModifiers)
48394848
{
48404849
reflection_object *intern;
48414850
zend_class_entry *ce;
4842-
uint32_t keep_flags = ZEND_ACC_FINAL
4843-
| ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
4851+
uint32_t keep_flags = ZEND_ACC_FINAL | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_READONLY_CLASS;
48444852

48454853
if (zend_parse_parameters_none() == FAILURE) {
48464854
RETURN_THROWS();
@@ -7075,6 +7083,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */
70757083
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_IMPLICIT_ABSTRACT", ZEND_ACC_IMPLICIT_ABSTRACT_CLASS);
70767084
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_EXPLICIT_ABSTRACT", ZEND_ACC_EXPLICIT_ABSTRACT_CLASS);
70777085
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_FINAL", ZEND_ACC_FINAL);
7086+
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_READONLY", ZEND_ACC_READONLY_CLASS);
70787087

70797088
reflection_object_ptr = register_class_ReflectionObject(reflection_class_ptr);
70807089
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
@@ -314,6 +314,8 @@ public function isAbstract(): bool {}
314314
/** @tentative-return-type */
315315
public function isFinal(): bool {}
316316

317+
public function isReadonly(): bool {}
318+
317319
/** @tentative-return-type */
318320
public function getModifiers(): int {}
319321

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: 62fcf63d2f3e93537560c3a03e71fda131a31586 */
2+
* Stub hash: e22dbd1c0ad761ddcaf6b516101bff8638782dc2 */
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)
@@ -256,6 +256,8 @@ ZEND_END_ARG_INFO()
256256

257257
#define arginfo_class_ReflectionClass_isFinal arginfo_class_ReflectionFunctionAbstract_inNamespace
258258

259+
#define arginfo_class_ReflectionClass_isReadonly arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType
260+
259261
#define arginfo_class_ReflectionClass_getModifiers arginfo_class_ReflectionFunctionAbstract_getNumberOfParameters
260262

261263
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_isInstance, 0, 1, _IS_BOOL, 0)
@@ -691,6 +693,7 @@ ZEND_METHOD(ReflectionClass, isTrait);
691693
ZEND_METHOD(ReflectionClass, isEnum);
692694
ZEND_METHOD(ReflectionClass, isAbstract);
693695
ZEND_METHOD(ReflectionClass, isFinal);
696+
ZEND_METHOD(ReflectionClass, isReadonly);
694697
ZEND_METHOD(ReflectionClass, getModifiers);
695698
ZEND_METHOD(ReflectionClass, isInstance);
696699
ZEND_METHOD(ReflectionClass, newInstance);
@@ -955,6 +958,7 @@ static const zend_function_entry class_ReflectionClass_methods[] = {
955958
ZEND_ME(ReflectionClass, isEnum, arginfo_class_ReflectionClass_isEnum, ZEND_ACC_PUBLIC)
956959
ZEND_ME(ReflectionClass, isAbstract, arginfo_class_ReflectionClass_isAbstract, ZEND_ACC_PUBLIC)
957960
ZEND_ME(ReflectionClass, isFinal, arginfo_class_ReflectionClass_isFinal, ZEND_ACC_PUBLIC)
961+
ZEND_ME(ReflectionClass, isReadonly, arginfo_class_ReflectionClass_isReadonly, ZEND_ACC_PUBLIC)
958962
ZEND_ME(ReflectionClass, getModifiers, arginfo_class_ReflectionClass_getModifiers, ZEND_ACC_PUBLIC)
959963
ZEND_ME(ReflectionClass, isInstance, arginfo_class_ReflectionClass_isInstance, ZEND_ACC_PUBLIC)
960964
ZEND_ME(ReflectionClass, newInstance, arginfo_class_ReflectionClass_newInstance, ZEND_ACC_PUBLIC)

0 commit comments

Comments
 (0)