Skip to content

Add support for readonly classes #7305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
The readonly class modifier can only be added once
--FILE--
<?php

readonly readonly class Foo
{
}

?>
--EXPECTF--
Fatal error: Multiple readonly modifiers are not allowed in %s on line %d
20 changes: 20 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_dynamic_property.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Readonly classes cannot use dynamic properties
--FILE--
<?php

readonly class Foo
{
}

$foo = new Foo();

try {
$foo->bar = 1;
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

?>
--EXPECT--
Cannot create dynamic property Foo::$bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Readonly classes cannot apply the #[AllowDynamicProperties] attribute
--FILE--
<?php

#[AllowDynamicProperties]
readonly class Foo
{
}

?>
--EXPECTF--
Fatal error: Cannot apply #[AllowDynamicProperties] to readonly class Foo in %s on line %d
16 changes: 16 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_final_modifier.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
The readonly and final class modifiers can be defined in the same time
--FILE--
<?php

final readonly class Foo
{
}

readonly class Bar extends Foo
{
}

?>
--EXPECTF--
Fatal error: Class Bar cannot extend final class Foo in %s on line %d
16 changes: 16 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_inheritance_error1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Non-readonly class cannot extend a readonly class
--FILE--
<?php

readonly class Foo
{
}

class Bar extends Foo
{
}

?>
--EXPECTF--
Fatal error: Non-readonly class Bar cannot extend readonly class Foo in %s on line %d
16 changes: 16 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_inheritance_error2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Readonly class cannot extend a non-readonly class
--FILE--
<?php

class Foo
{
}

readonly class Bar extends Foo
{
}

?>
--EXPECTF--
Fatal error: Readonly class Bar cannot extend non-readonly class Foo in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Readonly class can extend a readonly class
--FILE--
<?php

readonly class Foo
{
}

readonly class Bar extends Foo
{
}

?>
--EXPECT--
13 changes: 13 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_missing_type1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Normal properties of a readonly class must have type
--FILE--
<?php

readonly class Foo
{
public $bar;
}

?>
--EXPECTF--
Fatal error: Readonly property Foo::$bar must have type in %s on line %d
15 changes: 15 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_missing_type2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Promoted properties of a readonly class must have type
--FILE--
<?php

readonly class Foo
{
public function __construct(
private $bar
) {}
}

?>
--EXPECTF--
Fatal error: Readonly property Foo::$bar must have type in %s on line %d
25 changes: 25 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_property1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
Normal properties of a readonly class are implicitly declared as readonly
--FILE--
<?php

readonly class Foo
{
public int $bar;

public function __construct() {
$this->bar = 1;
}
}

$foo = new Foo();

try {
$foo->bar = 2;
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

?>
--EXPECT--
Cannot modify readonly property Foo::$bar
23 changes: 23 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_property2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Promoted properties of a readonly class are implicitly declared as readonly
--FILE--
<?php

readonly class Foo
{
public function __construct(
public int $bar
) {}
}

$foo = new Foo(1);

try {
$foo->bar = 2;
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

?>
--EXPECT--
Cannot modify readonly property Foo::$bar
13 changes: 13 additions & 0 deletions Zend/tests/readonly_classes/readonly_class_property3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Declaring static property for a readonly class is forbidden
--FILE--
<?php

readonly class Foo
{
public static int $bar;
}

?>
--EXPECTF--
Fatal error: Readonly class Foo cannot declare static properties in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/readonly_classes/readonly_enum.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Enums cannot be readonly
--FILE--
<?php

readonly enum Foo
{
}

?>
--EXPECTF--
Parse error: syntax error, unexpected token "enum", expecting "abstract" or "final" or "readonly" or "class" in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/readonly_classes/readonly_interface.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Interfaces cannot be readonly
--FILE--
<?php

readonly interface Foo
{
}

?>
--EXPECTF--
Parse error: syntax error, unexpected token "interface", expecting "abstract" or "final" or "readonly" or "class" in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/readonly_classes/readonly_trait.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Traits cannot be readonly
--FILE--
<?php

readonly trait Foo
{
}

?>
--EXPECTF--
Parse error: syntax error, unexpected token "trait", expecting "abstract" or "final" or "readonly" or "class" in %s on line %d
3 changes: 3 additions & 0 deletions Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
if (decl->flags & ZEND_ACC_FINAL) {
smart_str_appends(str, "final ");
}
if (decl->flags & ZEND_ACC_READONLY_CLASS) {
smart_str_appends(str, "readonly ");
}
smart_str_appends(str, "class ");
}
smart_str_appendl(str, ZSTR_VAL(decl->name), ZSTR_LEN(decl->name));
Expand Down
5 changes: 5 additions & 0 deletions Zend/zend_attributes.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ static void validate_allow_dynamic_properties(
if (scope->ce_flags & ZEND_ACC_INTERFACE) {
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to interface");
}
if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) {
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to readonly class %s",
ZSTR_VAL(scope->name)
);
}
scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES;
}

Expand Down
19 changes: 19 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,10 @@ uint32_t zend_add_class_modifier(uint32_t flags, uint32_t new_flag) /* {{{ */
zend_throw_exception(zend_ce_compile_error, "Multiple final modifiers are not allowed", 0);
return 0;
}
if ((flags & ZEND_ACC_READONLY_CLASS) && (new_flag & ZEND_ACC_READONLY_CLASS)) {
zend_throw_exception(zend_ce_compile_error, "Multiple readonly modifiers are not allowed", 0);
return 0;
}
if ((new_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) && (new_flags & ZEND_ACC_FINAL)) {
zend_throw_exception(zend_ce_compile_error,
"Cannot use the final modifier on an abstract class", 0);
Expand Down Expand Up @@ -6673,6 +6677,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
if (property_flags) {
zend_op_array *op_array = CG(active_op_array);
zend_class_entry *scope = op_array->scope;

bool is_ctor =
scope && zend_is_constructor(op_array->function_name);
if (!is_ctor) {
Expand All @@ -6699,6 +6704,10 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
ZSTR_VAL(scope->name), ZSTR_VAL(name), ZSTR_VAL(str));
}

if (!(property_flags & ZEND_ACC_READONLY) && (scope->ce_flags & ZEND_ACC_READONLY_CLASS)) {
property_flags |= ZEND_ACC_READONLY;
}

/* Recompile the type, as it has different memory management requirements. */
zend_type type = ZEND_TYPE_INIT_NONE(0);
if (type_ast) {
Expand Down Expand Up @@ -7241,6 +7250,12 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
zend_error_noreturn(E_COMPILE_ERROR, "Properties cannot be declared abstract");
}

if ((ce->ce_flags & ZEND_ACC_READONLY_CLASS) && (flags & ZEND_ACC_STATIC)) {
zend_error_noreturn(E_COMPILE_ERROR, "Readonly class %s cannot declare static properties",
ZSTR_VAL(ce->name)
);
}

for (i = 0; i < children; ++i) {
zend_property_info *info;
zend_ast *prop_ast = list->child[i];
Expand Down Expand Up @@ -7306,6 +7321,10 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
ZVAL_UNDEF(&value_zv);
}

if ((ce->ce_flags & ZEND_ACC_READONLY_CLASS)) {
flags |= ZEND_ACC_READONLY;
}

if (flags & ZEND_ACC_READONLY) {
if (!ZEND_TYPE_IS_SET(type)) {
zend_error_noreturn(E_COMPILE_ERROR, "Readonly property %s::$%s must have type",
Expand Down
5 changes: 4 additions & 1 deletion Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ typedef struct _zend_oparray_context {
/* or IS_CONSTANT_VISITED_MARK | | | */
#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */
/* | | | */
/* Class Flags (unused: 16,21,30,31) | | | */
/* Class Flags (unused: 21,30,31) | | | */
/* =========== | | | */
/* | | | */
/* Special class types | | | */
Expand Down Expand Up @@ -273,6 +273,9 @@ typedef struct _zend_oparray_context {
/* without triggering a deprecation warning | | | */
#define ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES (1 << 15) /* X | | | */
/* | | | */
/* Readonly class | | | */
#define ZEND_ACC_READONLY_CLASS (1 << 16) /* X | | | */
/* | | | */
/* Parent class is resolved (CE). | | | */
#define ZEND_ACC_RESOLVED_PARENT (1 << 17) /* X | | | */
/* | | | */
Expand Down
7 changes: 7 additions & 0 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,13 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
}
}

if (UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) {
zend_error_noreturn(E_COMPILE_ERROR, "%s class %s cannot extend %s class %s",
ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "Readonly" : "Non-readonly", ZSTR_VAL(ce->name),
parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "readonly" : "non-readonly", ZSTR_VAL(parent_ce->name)
);
}

if (ce->parent_name) {
zend_string_release_ex(ce->parent_name, 0);
}
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ class_modifiers:
class_modifier:
T_ABSTRACT { $$ = ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; }
| T_FINAL { $$ = ZEND_ACC_FINAL; }
| T_READONLY { $$ = ZEND_ACC_READONLY_CLASS|ZEND_ACC_NO_DYNAMIC_PROPERTIES; }
;

trait_declaration_statement:
Expand Down
4 changes: 4 additions & 0 deletions build/gen_stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,10 @@ private function getFlagsAsString(): string
$flags[] = "ZEND_ACC_ABSTRACT";
}

if ($this->flags & Class_::MODIFIER_READONLY) {
$flags[] = "ZEND_ACC_READONLY_CLASS";
}

if ($this->isDeprecated) {
$flags[] = "ZEND_ACC_DEPRECATED";
}
Expand Down
Loading