Add support for readonly classes (#7305)

RFC: https://wiki.php.net/rfc/readonly_classes
This commit is contained in:
Máté Kocsis 2022-05-16 20:40:23 +02:00 committed by GitHub
parent 6beee1a5bb
commit 7850c10389
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 376 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,15 @@
--TEST--
Readonly class can extend a readonly class
--FILE--
<?php
readonly class Foo
{
}
readonly class Bar extends Foo
{
}
?>
--EXPECT--

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1715,6 +1715,9 @@ tail_call:
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));

View File

@ -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;
}

View File

@ -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);
@ -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) {
@ -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) {
@ -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];
@ -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",

View File

@ -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 | | | */
@ -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 | | | */
/* | | | */

View File

@ -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);
}

View File

@ -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:

View File

@ -1797,6 +1797,10 @@ class ClassInfo {
$flags[] = "ZEND_ACC_ABSTRACT";
}
if ($this->flags & Class_::MODIFIER_READONLY) {
$flags[] = "ZEND_ACC_READONLY_CLASS";
}
if ($this->isDeprecated) {
$flags[] = "ZEND_ACC_DEPRECATED";
}

View File

@ -348,6 +348,9 @@ static void _class_string(smart_str *str, zend_class_entry *ce, zval *obj, char
if (ce->ce_flags & ZEND_ACC_FINAL) {
smart_str_append_printf(str, "final ");
}
if (ce->ce_flags & ZEND_ACC_READONLY_CLASS) {
smart_str_append_printf(str, "readonly ");
}
smart_str_append_printf(str, "class ");
}
smart_str_append_printf(str, "%s", ZSTR_VAL(ce->name));
@ -4863,6 +4866,12 @@ ZEND_METHOD(ReflectionClass, isFinal)
}
/* }}} */
/* Returns whether this class is readonly */
ZEND_METHOD(ReflectionClass, isReadOnly)
{
_class_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_READONLY_CLASS);
}
/* {{{ Returns whether this class is abstract */
ZEND_METHOD(ReflectionClass, isAbstract)
{
@ -4875,8 +4884,7 @@ ZEND_METHOD(ReflectionClass, getModifiers)
{
reflection_object *intern;
zend_class_entry *ce;
uint32_t keep_flags = ZEND_ACC_FINAL
| ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
uint32_t keep_flags = ZEND_ACC_FINAL | ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_READONLY_CLASS;
if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
@ -7123,6 +7131,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_IMPLICIT_ABSTRACT", ZEND_ACC_IMPLICIT_ABSTRACT_CLASS);
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_EXPLICIT_ABSTRACT", ZEND_ACC_EXPLICIT_ABSTRACT_CLASS);
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_FINAL", ZEND_ACC_FINAL);
REGISTER_REFLECTION_CLASS_CONST_LONG(class, "IS_READONLY", ZEND_ACC_READONLY_CLASS);
reflection_object_ptr = register_class_ReflectionObject(reflection_class_ptr);
reflection_object_ptr->create_object = reflection_objects_new;

View File

@ -318,6 +318,8 @@ class ReflectionClass implements Reflector
/** @tentative-return-type */
public function isFinal(): bool {}
public function isReadOnly(): bool {}
/** @tentative-return-type */
public function getModifiers(): int {}

View File

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: c9656b23db965e890e73d0064005b981ee1991cf */
* Stub hash: 4191864554b030bea40306c0d30090a8e2c76ab2 */
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0)
ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0)
@ -260,6 +260,8 @@ ZEND_END_ARG_INFO()
#define arginfo_class_ReflectionClass_isFinal arginfo_class_ReflectionFunctionAbstract_inNamespace
#define arginfo_class_ReflectionClass_isReadOnly arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType
#define arginfo_class_ReflectionClass_getModifiers arginfo_class_ReflectionFunctionAbstract_getNumberOfParameters
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);
ZEND_METHOD(ReflectionClass, isEnum);
ZEND_METHOD(ReflectionClass, isAbstract);
ZEND_METHOD(ReflectionClass, isFinal);
ZEND_METHOD(ReflectionClass, isReadOnly);
ZEND_METHOD(ReflectionClass, getModifiers);
ZEND_METHOD(ReflectionClass, isInstance);
ZEND_METHOD(ReflectionClass, newInstance);
@ -963,6 +966,7 @@ static const zend_function_entry class_ReflectionClass_methods[] = {
ZEND_ME(ReflectionClass, isEnum, arginfo_class_ReflectionClass_isEnum, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionClass, isAbstract, arginfo_class_ReflectionClass_isAbstract, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionClass, isFinal, arginfo_class_ReflectionClass_isFinal, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionClass, isReadOnly, arginfo_class_ReflectionClass_isReadOnly, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionClass, getModifiers, arginfo_class_ReflectionClass_getModifiers, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionClass, isInstance, arginfo_class_ReflectionClass_isInstance, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionClass, newInstance, arginfo_class_ReflectionClass_newInstance, ZEND_ACC_PUBLIC)

View File

@ -9,15 +9,17 @@ abstract class A {}
class B extends A {}
class C {}
final class D {}
readonly class E {}
interface I {}
$classes = array("A", "B", "C", "D", "I");
$classes = array("A", "B", "C", "D", "E", "I");
foreach ($classes as $class) {
$rc = new ReflectionClass($class);
var_dump($rc->isFinal());
var_dump($rc->isInterface());
var_dump($rc->isAbstract());
var_dump($rc->isReadOnly());
var_dump($rc->getModifiers());
}
?>
@ -25,20 +27,30 @@ foreach ($classes as $class) {
bool(false)
bool(false)
bool(true)
bool(false)
int(64)
bool(false)
bool(false)
bool(false)
bool(false)
int(0)
bool(false)
bool(false)
bool(false)
bool(false)
int(0)
bool(true)
bool(false)
bool(false)
bool(false)
int(32)
bool(false)
bool(false)
bool(false)
bool(true)
int(65536)
bool(false)
bool(true)
bool(false)
bool(false)
int(0)

View File

@ -11,10 +11,11 @@ echo $rc;
--EXPECT--
Class [ <internal:Reflection> class ReflectionClass implements Stringable, Reflector ] {
- Constants [3] {
- Constants [4] {
Constant [ public int IS_IMPLICIT_ABSTRACT ] { 16 }
Constant [ public int IS_EXPLICIT_ABSTRACT ] { 64 }
Constant [ public int IS_FINAL ] { 32 }
Constant [ public int IS_READONLY ] { 65536 }
}
- Static properties [0] {
@ -27,7 +28,7 @@ Class [ <internal:Reflection> class ReflectionClass implements Stringable, Refle
Property [ public string $name ]
}
- Methods [55] {
- Methods [56] {
Method [ <internal:Reflection> private method __clone ] {
- Parameters [0] {
@ -284,6 +285,13 @@ Class [ <internal:Reflection> class ReflectionClass implements Stringable, Refle
- Tentative return [ bool ]
}
Method [ <internal:Reflection> public method isReadOnly ] {
- Parameters [0] {
}
- Return [ bool ]
}
Method [ <internal:Reflection> public method getModifiers ] {
- Parameters [0] {

View File

@ -0,0 +1,34 @@
--TEST--
Using ReflectionClass::__toString() on readonly classes
--FILE--
<?php
readonly class Foo {
public int $bar;
public readonly int $baz;
}
echo new ReflectionClass(Foo::class);
?>
--EXPECTF--
Class [ <user> readonly class Foo ] {
@@ %s 3-6
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [2] {
Property [ public readonly int $bar ]
Property [ public readonly int $baz ]
}
- Methods [0] {
}
}

View File

@ -0,0 +1,25 @@
--TEST--
Readonly class reflection
--FILE--
<?php
class Foo {
}
readonly class Bar {
}
$foo = new ReflectionClass(Foo::class);
var_dump($foo->isReadOnly());
var_dump(($foo->getModifiers() & ReflectionClass::IS_READONLY) != 0);
$bar = new ReflectionClass(Bar::class);
var_dump($bar->isReadOnly());
var_dump(($bar->getModifiers() & ReflectionClass::IS_READONLY) != 0);
?>
--EXPECT--
bool(false)
bool(false)
bool(true)
bool(true)