Skip to content

Commit 1e3d5b8

Browse files
committed
Add support for readonly classes
1 parent 3babe95 commit 1e3d5b8

17 files changed

+206
-6
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+
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 readonly $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

Zend/zend_ast.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
16991699
if (decl->flags & ZEND_ACC_FINAL) {
17001700
smart_str_appends(str, "final ");
17011701
}
1702+
if (decl->flags & ZEND_ACC_READONLY_CLASS) {
1703+
smart_str_appends(str, "readonly ");
1704+
}
17021705
smart_str_appends(str, "class ");
17031706
}
17041707
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) && (CG(active_class_entry)->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) && (CG(active_class_entry)->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: 30...) | | | */
244+
/* Class Flags (unused: 31...) | | | */
245245
/* =========== | | | */
246246
/* | | | */
247247
/* Special class types | | | */
@@ -307,6 +307,9 @@ typedef struct _zend_oparray_context {
307307
/* Class cannot be serialized or unserialized | | | */
308308
#define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */
309309
/* | | | */
310+
/* Readonly class | | | */
311+
#define ZEND_ACC_READONLY_CLASS (1 << 30) /* X | | | */
312+
/* | | | */
310313
/* Function Flags (unused: 27-30) | | | */
311314
/* ============== | | | */
312315
/* | | | */

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: 10 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) {
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);
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;
48594867

48604868
if (zend_parse_parameters_none() == FAILURE) {
48614869
RETURN_THROWS();

ext/reflection/php_reflection.stub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@ public function isAbstract(): bool {}
314314
/** @tentative-return-type */
315315
public function isFinal(): bool {}
316316

317+
/** @tentative-return-type */
318+
public function isReadonly(): bool {}
319+
317320
/** @tentative-return-type */
318321
public function getModifiers(): int {}
319322

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: 6c835a06c681b780b3c26f1fb5fcd6731eb1e90d */
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_inNamespace
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)

ext/reflection/tests/ReflectionClass_modifiers_001.phpt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,48 @@ abstract class A {}
99
class B extends A {}
1010
class C {}
1111
final class D {}
12+
readonly class E {}
1213
interface I {}
1314

14-
$classes = array("A", "B", "C", "D", "I");
15+
$classes = array("A", "B", "C", "D", "E", "I");
1516

1617
foreach ($classes as $class) {
1718
$rc = new ReflectionClass($class);
1819
var_dump($rc->isFinal());
1920
var_dump($rc->isInterface());
2021
var_dump($rc->isAbstract());
22+
var_dump($rc->isReadonly());
2123
var_dump($rc->getModifiers());
2224
}
2325
?>
2426
--EXPECT--
2527
bool(false)
2628
bool(false)
2729
bool(true)
30+
bool(false)
2831
int(64)
2932
bool(false)
3033
bool(false)
3134
bool(false)
35+
bool(false)
3236
int(0)
3337
bool(false)
3438
bool(false)
3539
bool(false)
40+
bool(false)
3641
int(0)
3742
bool(true)
3843
bool(false)
3944
bool(false)
45+
bool(false)
4046
int(32)
4147
bool(false)
48+
bool(false)
49+
bool(false)
50+
bool(true)
51+
int(128)
52+
bool(false)
4253
bool(true)
4354
bool(false)
55+
bool(false)
4456
int(0)

ext/reflection/tests/ReflectionClass_toString_001.phpt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Class [ <internal:Reflection> class ReflectionClass implements Reflector, String
2727
Property [ public string $name ]
2828
}
2929

30-
- Methods [55] {
30+
- Methods [56] {
3131
Method [ <internal:Reflection> private method __clone ] {
3232

3333
- Parameters [0] {
@@ -284,6 +284,13 @@ Class [ <internal:Reflection> class ReflectionClass implements Reflector, String
284284
- Tentative return [ bool ]
285285
}
286286

287+
Method [ <internal:Reflection> public method isReadonly ] {
288+
289+
- Parameters [0] {
290+
}
291+
- Tentative return [ bool ]
292+
}
293+
287294
Method [ <internal:Reflection> public method getModifiers ] {
288295

289296
- Parameters [0] {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
Using ReflectionClass::__toString() on readonly classes
3+
--FILE--
4+
<?php
5+
6+
readonly class Foo {
7+
public int $bar;
8+
public readonly int $baz;
9+
}
10+
11+
echo new ReflectionClass(Foo::class);
12+
13+
?>
14+
--EXPECTF--
15+
Class [ <user> readonly class Foo ] {
16+
@@ %s 3-6
17+
18+
- Constants [0] {
19+
}
20+
21+
- Static properties [0] {
22+
}
23+
24+
- Static methods [0] {
25+
}
26+
27+
- Properties [2] {
28+
Property [ public readonly int $bar ]
29+
Property [ public readonly int $baz ]
30+
}
31+
32+
- Methods [0] {
33+
}
34+
}

0 commit comments

Comments
 (0)