Skip to content

Commit 5ab999c

Browse files
committed
Add support for readonly classes
1 parent abacd91 commit 5ab999c

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
@@ -1695,6 +1695,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
16951695
if (decl->flags & ZEND_ACC_FINAL) {
16961696
smart_str_appends(str, "final ");
16971697
}
1698+
if (decl->flags & ZEND_ACC_READONLY_CLASS) {
1699+
smart_str_appends(str, "readonly ");
1700+
}
16981701
smart_str_appends(str, "class ");
16991702
}
17001703
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
@@ -794,6 +794,10 @@ uint32_t zend_add_class_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */
794794
zend_throw_exception(zend_ce_compile_error, "Multiple final modifiers are not allowed", 0);
795795
return 0;
796796
}
797+
if ((flags & ZEND_ACC_READONLY_CLASS) && (new_flag & ZEND_ACC_READONLY_CLASS)) {
798+
zend_throw_exception(zend_ce_compile_error, "Multiple readonly modifiers are not allowed", 0);
799+
return 0;
800+
}
797801
if ((new_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) && (new_flags & ZEND_ACC_FINAL)) {
798802
zend_throw_exception(zend_ce_compile_error,
799803
"Cannot use the final modifier on an abstract class", 0);
@@ -6736,6 +6740,7 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
67366740
if (property_flags) {
67376741
zend_op_array *op_array = CG(active_op_array);
67386742
zend_class_entry *scope = op_array->scope;
6743+
67396744
bool is_ctor =
67406745
scope && zend_is_constructor(op_array->function_name);
67416746
if (!is_ctor) {
@@ -6762,6 +6767,10 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
67626767
ZSTR_VAL(scope->name), ZSTR_VAL(name), ZSTR_VAL(str));
67636768
}
67646769

6770+
if (!(property_flags & ZEND_ACC_READONLY) && (scope->ce_flags & ZEND_ACC_READONLY_CLASS)) {
6771+
property_flags |= ZEND_ACC_READONLY;
6772+
}
6773+
67656774
/* Recompile the type, as it has different memory management requirements. */
67666775
zend_type type = ZEND_TYPE_INIT_NONE(0);
67676776
if (type_ast) {
@@ -7364,6 +7373,10 @@ void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t flags, z
73647373
ZVAL_UNDEF(&value_zv);
73657374
}
73667375

7376+
if (!(flags & ZEND_ACC_READONLY) && (ce->ce_flags & ZEND_ACC_READONLY_CLASS)) {
7377+
flags |= ZEND_ACC_READONLY;
7378+
}
7379+
73677380
if (flags & ZEND_ACC_READONLY) {
73687381
if (!ZEND_TYPE_IS_SET(type)) {
73697382
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
@@ -241,7 +241,7 @@ typedef struct _zend_oparray_context {
241241
/* or IS_CONSTANT_VISITED_MARK | | | */
242242
#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */
243243
/* | | | */
244-
/* Class Flags (unused: 15,30,31) | | | */
244+
/* Class Flags (unused: 30,31) | | | */
245245
/* =========== | | | */
246246
/* | | | */
247247
/* Special class types | | | */
@@ -270,6 +270,9 @@ typedef struct _zend_oparray_context {
270270
/* User class has methods with static variables | | | */
271271
#define ZEND_HAS_STATIC_IN_METHODS (1 << 14) /* X | | | */
272272
/* | | | */
273+
/* Readonly class | | | */
274+
#define ZEND_ACC_READONLY_CLASS (1 << 15) /* X | | | */
275+
/* | | | */
273276
/* Children must reuse parent get_iterator() | | | */
274277
#define ZEND_ACC_REUSE_GET_ITERATOR (1 << 16) /* X | | | */
275278
/* | | | */

Zend/zend_inheritance.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,13 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
14401440
}
14411441
}
14421442

1443+
if (UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) {
1444+
zend_error_noreturn(E_COMPILE_ERROR, "%s class %s cannot extend %s class %s",
1445+
ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "Readonly" : "Non-readonly", ZSTR_VAL(ce->name),
1446+
parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "readonly" : "non-readonly", ZSTR_VAL(parent_ce->name)
1447+
);
1448+
}
1449+
14431450
if (ce->parent_name) {
14441451
zend_string_release_ex(ce->parent_name, 0);
14451452
}

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
@@ -1318,6 +1318,10 @@ private function getFlagsAsString(): string
13181318
$flags[] = "ZEND_ACC_ABSTRACT";
13191319
}
13201320

1321+
if ($this->flags & Class_::MODIFIER_READONLY) {
1322+
$flags[] = "ZEND_ACC_READONLY_CLASS";
1323+
}
1324+
13211325
if ($this->isDeprecated) {
13221326
$flags[] = "ZEND_ACC_DEPRECATED";
13231327
}

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));
@@ -4842,6 +4845,12 @@ ZEND_METHOD(ReflectionClass, isFinal)
48424845
}
48434846
/* }}} */
48444847

4848+
/* Returns whether this class is readonly */
4849+
ZEND_METHOD(ReflectionClass, isReadonly)
4850+
{
4851+
_class_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_READONLY_CLASS);
4852+
}
4853+
48454854
/* {{{ Returns whether this class is abstract */
48464855
ZEND_METHOD(ReflectionClass, isAbstract)
48474856
{
@@ -4854,8 +4863,7 @@ ZEND_METHOD(ReflectionClass, getModifiers)
48544863
{
48554864
reflection_object *intern;
48564865
zend_class_entry *ce;
4857-
uint32_t keep_flags = ZEND_ACC_FINAL
4858-
| ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
4866+
uint32_t keep_flags = ZEND_ACC_FINAL | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_READONLY_CLASS;
48594867

48604868
if (zend_parse_parameters_none() == FAILURE) {
48614869
RETURN_THROWS();
@@ -7101,6 +7109,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */
71017109
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_IMPLICIT_ABSTRACT", ZEND_ACC_IMPLICIT_ABSTRACT_CLASS);
71027110
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_EXPLICIT_ABSTRACT", ZEND_ACC_EXPLICIT_ABSTRACT_CLASS);
71037111
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_FINAL", ZEND_ACC_FINAL);
7112+
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_READONLY", ZEND_ACC_READONLY_CLASS);
71047113

71057114
reflection_object_ptr = register_class_ReflectionObject(reflection_class_ptr);
71067115
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: aae05073f9a7898d836023183809faa265cb99c3 */
2+
* Stub hash: 7c18337adac48719ee1da9f50054648c177f7140 */
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)
@@ -694,6 +696,7 @@ ZEND_METHOD(ReflectionClass, isTrait);
694696
ZEND_METHOD(ReflectionClass, isEnum);
695697
ZEND_METHOD(ReflectionClass, isAbstract);
696698
ZEND_METHOD(ReflectionClass, isFinal);
699+
ZEND_METHOD(ReflectionClass, isReadonly);
697700
ZEND_METHOD(ReflectionClass, getModifiers);
698701
ZEND_METHOD(ReflectionClass, isInstance);
699702
ZEND_METHOD(ReflectionClass, newInstance);
@@ -958,6 +961,7 @@ static const zend_function_entry class_ReflectionClass_methods[] = {
958961
ZEND_ME(ReflectionClass, isEnum, arginfo_class_ReflectionClass_isEnum, ZEND_ACC_PUBLIC)
959962
ZEND_ME(ReflectionClass, isAbstract, arginfo_class_ReflectionClass_isAbstract, ZEND_ACC_PUBLIC)
960963
ZEND_ME(ReflectionClass, isFinal, arginfo_class_ReflectionClass_isFinal, ZEND_ACC_PUBLIC)
964+
ZEND_ME(ReflectionClass, isReadonly, arginfo_class_ReflectionClass_isReadonly, ZEND_ACC_PUBLIC)
961965
ZEND_ME(ReflectionClass, getModifiers, arginfo_class_ReflectionClass_getModifiers, ZEND_ACC_PUBLIC)
962966
ZEND_ME(ReflectionClass, isInstance, arginfo_class_ReflectionClass_isInstance, ZEND_ACC_PUBLIC)
963967
ZEND_ME(ReflectionClass, newInstance, arginfo_class_ReflectionClass_newInstance, ZEND_ACC_PUBLIC)

0 commit comments

Comments
 (0)