Merge branch 'master' of github.com:php/php-src

* 'master' of github.com:php/php-src:
  Implement readonly properties
This commit is contained in:
Joe Watkins 2021-07-20 12:32:51 +02:00
commit 27bb57356c
No known key found for this signature in database
GPG Key ID: F9BA0ADA31CBD89E
42 changed files with 1118 additions and 40 deletions

View File

@ -206,6 +206,8 @@ PHP 8.1 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/pure-intersection-types
. Added support for the final modifier for class constants.
RFC: https://wiki.php.net/rfc/final_class_const
. Added support for readonly properties.
RFC: https://wiki.php.net/rfc/readonly_properties_v2
- Curl:
. Added CURLOPT_DOH_URL option.

View File

@ -45,6 +45,7 @@ class Obj
function array(){ echo __METHOD__, PHP_EOL; }
function print(){ echo __METHOD__, PHP_EOL; }
function echo(){ echo __METHOD__, PHP_EOL; }
function readonly(){ echo __METHOD__, PHP_EOL; }
function require(){ echo __METHOD__, PHP_EOL; }
function require_once(){ echo __METHOD__, PHP_EOL; }
function return(){ echo __METHOD__, PHP_EOL; }
@ -125,6 +126,7 @@ $obj->throw();
$obj->array();
$obj->print();
$obj->echo();
$obj->readonly();
$obj->require();
$obj->require_once();
$obj->return();
@ -205,6 +207,7 @@ Obj::throw
Obj::array
Obj::print
Obj::echo
Obj::readonly
Obj::require
Obj::require_once
Obj::return

View File

@ -0,0 +1,29 @@
--TEST--
By-ref foreach over readonly property
--FILE--
<?php
class Test {
public readonly int $prop;
public function init() {
$this->prop = 1;
}
}
$test = new Test;
// Okay, as foreach skips over uninitialized properties.
foreach ($test as &$prop) {}
$test->init();
try {
foreach ($test as &$prop) {}
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
Cannot acquire reference to readonly property Test::$prop

View File

@ -0,0 +1,122 @@
--TEST--
Test interaction with cache slots
--FILE--
<?php
class Test {
public readonly string $prop;
public readonly array $prop2;
public readonly object $prop3;
public function setProp(string $prop) {
$this->prop = $prop;
}
public function initAndAppendProp2() {
$this->prop2 = [];
$this->prop2[] = 1;
}
public function initProp3() {
$this->prop3 = new stdClass;
$this->prop3->foo = 1;
}
public function replaceProp3() {
$ref =& $this->prop3;
$ref = new stdClass;
}
}
$test = new Test;
$test->setProp("a");
var_dump($test->prop);
try {
$test->setProp("b");
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop);
echo "\n";
$test = new Test;
try {
$test->initAndAppendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->initAndAppendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop2);
echo "\n";
$test = new Test;
$test->initProp3();
$test->replaceProp3();
var_dump($test->prop3);
$test->replaceProp3();
var_dump($test->prop3);
echo "\n";
// Test variations using closure rebinding, so we have unknown property_info in JIT.
$test = new Test;
(function() { $this->prop2 = []; })->call($test);
$appendProp2 = (function() {
$this->prop2[] = 1;
})->bindTo($test, Test::class);
try {
$appendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$appendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop2);
echo "\n";
$test = new Test;
$replaceProp3 = (function() {
$ref =& $this->prop3;
$ref = new stdClass;
})->bindTo($test, Test::class);
$test->initProp3();
$replaceProp3();
var_dump($test->prop3);
$replaceProp3();
var_dump($test->prop3);
?>
--EXPECT--
string(1) "a"
Cannot modify readonly property Test::$prop
string(1) "a"
Cannot modify readonly property Test::$prop2
Cannot modify readonly property Test::$prop2
array(0) {
}
object(stdClass)#3 (1) {
["foo"]=>
int(1)
}
object(stdClass)#3 (1) {
["foo"]=>
int(1)
}
Cannot modify readonly property Test::$prop2
Cannot modify readonly property Test::$prop2
array(0) {
}
object(stdClass)#5 (1) {
["foo"]=>
int(1)
}
object(stdClass)#5 (1) {
["foo"]=>
int(1)
}

View File

@ -0,0 +1,72 @@
--TEST--
Initialization can only happen from private scope
--FILE--
<?php
class A {
public readonly int $prop;
public function initPrivate() {
$this->prop = 3;
}
}
class B extends A {
public function initProtected() {
$this->prop = 2;
}
}
$test = new B;
try {
$test->prop = 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->initProtected();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
$test->initPrivate();
var_dump($test->prop);
// Rebinding bypass works.
$test = new B;
(function() {
$this->prop = 1;
})->bindTo($test, A::class)();
var_dump($test->prop);
class C extends A {
public readonly int $prop;
}
$test = new C;
$test->initPrivate();
var_dump($test->prop);
class X {
public function initFromParent() {
$this->prop = 1;
}
}
class Y extends X {
public readonly int $prop;
}
$test = new Y;
try {
$test->initFromParent();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
Cannot initialize readonly property A::$prop from global scope
Cannot initialize readonly property A::$prop from scope B
int(3)
int(1)
int(3)
Cannot initialize readonly property Y::$prop from scope X

View File

@ -0,0 +1,74 @@
--TEST--
Interaction with magic get/set
--FILE--
<?php
class Test {
public readonly int $prop;
public function unsetProp() {
unset($this->prop);
}
public function __get($name) {
echo __METHOD__, "($name)\n";
return 1;
}
public function __set($name, $value) {
echo __METHOD__, "($name, $value)\n";
}
public function __unset($name) {
echo __METHOD__, "($name)\n";
}
public function __isset($name) {
echo __METHOD__, "($name)\n";
return true;
}
}
$test = new Test;
// The property is in uninitialized state, no magic methods should be invoked.
var_dump(isset($test->prop));
try {
var_dump($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop = 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
$test->unsetProp();
var_dump(isset($test->prop));
var_dump($test->prop);
$test->prop = 2;
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
bool(false)
Typed property Test::$prop must not be accessed before initialization
Cannot initialize readonly property Test::$prop from global scope
Cannot unset readonly property Test::$prop from global scope
Test::__isset(prop)
bool(true)
Test::__get(prop)
int(1)
Test::__set(prop, 2)
Test::__unset(prop)

View File

@ -0,0 +1,26 @@
--TEST--
Can override readonly property with attributes
--FILE--
<?php
#[Attribute]
class FooAttribute {}
class A {
public readonly int $prop;
public function __construct() {
$this->prop = 42;
}
}
class B extends A {
#[FooAttribute]
public readonly int $prop;
}
var_dump((new ReflectionProperty(B::class, 'prop'))->getAttributes()[0]->newInstance());
?>
--EXPECT--
object(FooAttribute)#1 (0) {
}

View File

@ -0,0 +1,41 @@
--TEST--
Promoted readonly property
--FILE--
<?php
class Point {
public function __construct(
public readonly float $x = 0.0,
public readonly float $y = 0.0,
public readonly float $z = 0.0,
) {}
}
var_dump(new Point);
$point = new Point(1.0, 2.0, 3.0);
try {
$point->x = 4.0;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($point);
?>
--EXPECT--
object(Point)#1 (3) {
["x"]=>
float(0)
["y"]=>
float(0)
["z"]=>
float(0)
}
Cannot modify readonly property Point::$x
object(Point)#1 (3) {
["x"]=>
float(1)
["y"]=>
float(2)
["z"]=>
float(3)
}

View File

@ -0,0 +1,12 @@
--TEST--
Class constants cannot be readonly
--FILE--
<?php
class Test {
readonly const X = 1;
}
?>
--EXPECTF--
Fatal error: Cannot use 'readonly' as constant modifier in %s on line %d

View File

@ -0,0 +1,67 @@
--TEST--
Not-modifying a readonly property holding an object
--FILE--
<?php
class Test {
public readonly object $prop;
public function __construct(object $prop) {
$this->prop = $prop;
}
}
$test = new Test(new stdClass);
$test->prop->foo = 1;
$test->prop->foo += 1;
$test->prop->foo++;
try {
$test->prop += 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop++;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
--$test->prop;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop);
// Unfortunately this is allowed, but does not modify $test->prop.
$ref =& $test->prop;
$ref = new stdClass;
var_dump($test->prop);
$test = new Test(new ArrayObject());
$test->prop[] = [];
$test->prop[0][] = 1;
var_dump($test->prop);
?>
--EXPECT--
Unsupported operand types: stdClass + int
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
object(stdClass)#2 (1) {
["foo"]=>
int(3)
}
object(stdClass)#2 (1) {
["foo"]=>
int(3)
}
object(ArrayObject)#7 (1) {
["storage":"ArrayObject":private]=>
array(1) {
[0]=>
array(1) {
[0]=>
int(1)
}
}
}

View File

@ -0,0 +1,12 @@
--TEST--
Method cannot be readonly
--FILE--
<?php
class Test {
readonly function test() {}
}
?>
--EXPECTF--
Fatal error: Cannot use 'readonly' as method modifier in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Method cannot be readonly in trait alias
--FILE--
<?php
class Test {
use T { foo as readonly; }
}
?>
--EXPECTF--
Fatal error: Cannot use 'readonly' as method modifier in %s on line %d

View File

@ -0,0 +1,82 @@
--TEST--
Modifying a readonly property
--FILE--
<?php
class Test {
readonly public int $prop;
readonly public array $prop2;
public function __construct() {
// Initializing assignments.
$this->prop = 1;
$this->prop2 = [];
}
}
function byRef(&$ref) {}
$test = new Test;
var_dump($test->prop); // Read.
try {
$test->prop = 2;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop += 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop++;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
++$test->prop;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$ref =& $test->prop;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop =& $ref;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
byRef($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop2); // Read.
try {
$test->prop2[] = 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop2[0][] = 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
int(1)
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
Cannot modify readonly property Test::$prop
array(0) {
}
Cannot modify readonly property Test::$prop2
Cannot modify readonly property Test::$prop2

View File

@ -0,0 +1,15 @@
--TEST--
Cannot replace readonly with readwrite
--FILE--
<?php
class A {
public readonly int $prop;
}
class B extends A {
public int $prop;
}
?>
--EXPECTF--
Fatal error: Cannot redeclare readonly property A::$prop as non-readonly B::$prop in %s on line %d

View File

@ -0,0 +1,19 @@
--TEST--
Readonly match of imported trait properties (valid)
--FILE--
<?php
trait T1 {
public readonly int $prop;
}
trait T2 {
public readonly int $prop;
}
class C {
use T1, T2;
}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,18 @@
--TEST--
Readonly mismatch of imported trait properties
--FILE--
<?php
trait T1 {
public int $prop;
}
trait T2 {
public readonly int $prop;
}
class C {
use T1, T2;
}
?>
--EXPECTF--
Fatal error: T1 and T2 define the same property ($prop) in the composition of C. However, the definition differs and is considered incompatible. Class was composed in %s on line %d

View File

@ -0,0 +1,19 @@
--TEST--
Readonly property with default value
--FILE--
<?php
class Test {
public readonly int $prop = 1;
}
$test = new Test;
try {
$test->prop = 2;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECTF--
Fatal error: Readonly property Test::$prop cannot have default value in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Readonly property without type
--FILE--
<?php
class Test {
protected readonly $prop;
}
?>
--EXPECTF--
Fatal error: Readonly property Test::$prop must have type in %s on line %d

View File

@ -0,0 +1,15 @@
--TEST--
Cannot replace readwrite with readonly
--FILE--
<?php
class A {
public int $prop;
}
class B extends A {
public readonly int $prop;
}
?>
--EXPECTF--
Fatal error: Cannot redeclare non-readonly property A::$prop as readonly B::$prop in %s on line %d

View File

@ -0,0 +1,34 @@
--TEST--
Serialization of readonly properties
--FILE--
<?php
class Test {
public function __construct(
public readonly int $prop = 1,
) {}
}
var_dump($s = serialize(new Test));
var_dump(unserialize($s));
// Readonly properties receive no special handling.
// What happens during unserialization stays in unserialization.
var_dump(unserialize("O:4:\"Test\":1:{s:4:\"prop\";i:2;}"));
var_dump(unserialize("O:4:\"Test\":2:{s:4:\"prop\";i:2;s:4:\"prop\";i:3;}"));
?>
--EXPECT--
string(30) "O:4:"Test":1:{s:4:"prop";i:1;}"
object(Test)#1 (1) {
["prop"]=>
int(1)
}
object(Test)#1 (1) {
["prop"]=>
int(2)
}
object(Test)#1 (1) {
["prop"]=>
int(3)
}

View File

@ -0,0 +1,12 @@
--TEST--
Readonly static property
--FILE--
<?php
class Test {
public static readonly int $prop;
}
?>
--EXPECTF--
Fatal error: Static property Test::$prop cannot be readonly in %s on line %d

View File

@ -0,0 +1,64 @@
--TEST--
Unset readonly property
--FILE--
<?php
class Test {
public readonly int $prop;
public function __construct(int $prop) {
$this->prop = $prop;
}
}
$test = new Test(1);
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
class Test2 {
public readonly int $prop;
public function __construct() {
unset($this->prop); // Unset uninitialized.
unset($this->prop); // Unset unset.
}
public function __get($name) {
// Lazy init.
echo __METHOD__, "\n";
$this->prop = 1;
return $this->prop;
}
}
$test = new Test2;
var_dump($test->prop); // Call __get.
var_dump($test->prop); // Don't call __get.
try {
unset($test->prop); // Unset initialized, illegal.
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
class Test3 {
public readonly int $prop;
}
$test = new Test3;
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
Cannot unset readonly property Test::$prop
Test2::__get
int(1)
int(1)
Cannot unset readonly property Test2::$prop
Cannot unset readonly property Test3::$prop from global scope

View File

@ -0,0 +1,30 @@
--TEST--
Visibility can change in readonly property
--FILE--
<?php
class A {
protected readonly int $prop;
public function __construct() {
$this->prop = 42;
}
}
class B extends A {
public readonly int $prop;
}
$a = new A();
try {
var_dump($a->prop);
} catch (Error $error) {
echo $error->getMessage() . "\n";
}
$b = new B();
var_dump($b->prop);
?>
--EXPECT--
Cannot access protected property A::$prop
int(42)

View File

@ -1757,6 +1757,9 @@ simple_list:
if (ast->attr & ZEND_ACC_STATIC) {
smart_str_appends(str, "static ");
}
if (ast->attr & ZEND_ACC_READONLY) {
smart_str_appends(str, "readonly ");
}
if (type_ast) {
zend_ast_export_type(str, type_ast, indent);

View File

@ -6598,8 +6598,7 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
zend_string *name = zval_make_interned_string(zend_ast_get_zval(var_ast));
bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
bool is_variadic = (param_ast->attr & ZEND_PARAM_VARIADIC) != 0;
uint32_t visibility =
param_ast->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);
uint32_t property_flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_READONLY);
znode var_node, default_node;
zend_uchar opcode;
@ -6681,7 +6680,7 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
if (type_ast) {
uint32_t default_type = *default_ast_ptr ? Z_TYPE(default_node.u.constant) : IS_UNDEF;
bool force_nullable = default_type == IS_NULL && !visibility;
bool force_nullable = default_type == IS_NULL && !property_flags;
op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS;
arg_info->type = zend_compile_typename(type_ast, force_nullable);
@ -6724,14 +6723,14 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
}
uint32_t arg_info_flags = _ZEND_ARG_INFO_FLAGS(is_ref, is_variadic, /* is_tentative */ 0)
| (visibility ? _ZEND_IS_PROMOTED_BIT : 0);
| (property_flags ? _ZEND_IS_PROMOTED_BIT : 0);
ZEND_TYPE_FULL_MASK(arg_info->type) |= arg_info_flags;
if (opcode == ZEND_RECV) {
opline->op2.num = type_ast ?
ZEND_TYPE_FULL_MASK(arg_info->type) : MAY_BE_ANY;
}
if (visibility) {
if (property_flags) {
zend_op_array *op_array = CG(active_op_array);
zend_class_entry *scope = op_array->scope;
bool is_ctor =
@ -6778,7 +6777,7 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
zend_string *doc_comment =
doc_comment_ast ? zend_string_copy(zend_ast_get_str(doc_comment_ast)) : NULL;
zend_property_info *prop = zend_declare_typed_property(
scope, name, &default_value, visibility | ZEND_ACC_PROMOTED, doc_comment, type);
scope, name, &default_value, property_flags | ZEND_ACC_PROMOTED, doc_comment, type);
if (attributes_ast) {
zend_compile_attributes(
&prop->attributes, attributes_ast, 0, ZEND_ATTRIBUTE_TARGET_PROPERTY);
@ -6799,9 +6798,8 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
for (i = 0; i < list->children; i++) {
zend_ast *param_ast = list->child[i];
bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
uint32_t visibility =
param_ast->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);
if (!visibility) {
uint32_t flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_READONLY);
if (!flags) {
continue;
}
@ -7037,6 +7035,10 @@ zend_string *zend_begin_method_decl(zend_op_array *op_array, zend_string *name,
zend_string *lcname;
if (fn_flags & ZEND_ACC_READONLY) {
zend_error(E_COMPILE_ERROR, "Cannot use 'readonly' as method modifier");
}
if ((fn_flags & ZEND_ACC_PRIVATE) && (fn_flags & ZEND_ACC_FINAL) && !zend_is_constructor(name)) {
zend_error(E_COMPILE_WARNING, "Private methods cannot be final as they are never overridden by other classes");
}
@ -7354,6 +7356,23 @@ void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t flags, z
ZVAL_UNDEF(&value_zv);
}
if (flags & ZEND_ACC_READONLY) {
if (!ZEND_TYPE_IS_SET(type)) {
zend_error_noreturn(E_COMPILE_ERROR, "Readonly property %s::$%s must have type",
ZSTR_VAL(ce->name), ZSTR_VAL(name));
}
if (!Z_ISUNDEF(value_zv)) {
zend_error_noreturn(E_COMPILE_ERROR,
"Readonly property %s::$%s cannot have default value",
ZSTR_VAL(ce->name), ZSTR_VAL(name));
}
if (flags & ZEND_ACC_STATIC) {
zend_error_noreturn(E_COMPILE_ERROR,
"Static property %s::$%s cannot be readonly",
ZSTR_VAL(ce->name), ZSTR_VAL(name));
}
}
info = zend_declare_typed_property(ce, name, &value_zv, flags, doc_comment, type);
if (attr_ast) {
@ -7381,6 +7400,8 @@ static void zend_check_const_and_trait_alias_attr(uint32_t attr, const char* ent
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use 'abstract' as %s modifier", entity);
} else if (attr & ZEND_ACC_FINAL) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use 'final' as %s modifier", entity);
} else if (attr & ZEND_ACC_READONLY) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use 'readonly' as %s modifier", entity);
}
}
/* }}} */
@ -7406,7 +7427,7 @@ void zend_compile_class_const_decl(zend_ast *ast, uint32_t flags, zend_ast *attr
zend_string *doc_comment = doc_comment_ast ? zend_string_copy(zend_ast_get_str(doc_comment_ast)) : NULL;
zval value_zv;
if (UNEXPECTED(flags & (ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT))) {
if (UNEXPECTED(flags & (ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT|ZEND_ACC_READONLY))) {
zend_check_const_and_trait_alias_attr(flags, "constant");
}

View File

@ -220,6 +220,9 @@ typedef struct _zend_oparray_context {
#define ZEND_ACC_ABSTRACT (1 << 6) /* X | X | | */
#define ZEND_ACC_EXPLICIT_ABSTRACT_CLASS (1 << 6) /* X | | | */
/* | | | */
/* Readonly property | | | */
#define ZEND_ACC_READONLY (1 << 7) /* | | X | */
/* | | | */
/* Immutable op_array and class_entries | | | */
/* (implemented only for lazy loading of op_arrays) | | | */
#define ZEND_ACC_IMMUTABLE (1 << 7) /* X | X | | */

View File

@ -823,6 +823,12 @@ ZEND_COLD zend_never_inline void zend_verify_property_type_error(zend_property_i
zend_string_release(type_str);
}
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_modification_error(
zend_property_info *info) {
zend_throw_error(NULL, "Cannot modify readonly property %s::$%s",
ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name));
}
static zend_class_entry *resolve_single_class_type(zend_string *name, zend_class_entry *self_ce) {
if (zend_string_equals_literal_ci(name, "self")) {
/* We need to explicitly check for this here, to avoid updating the type in the trait and
@ -927,6 +933,11 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf
{
zval tmp;
if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
zend_readonly_property_modification_error(info);
return &EG(uninitialized_zval);
}
ZVAL_DEREF(value);
ZVAL_COPY(&tmp, value);
@ -1884,7 +1895,9 @@ static zend_never_inline void zend_post_incdec_overloaded_property(zend_object *
object->handlers->write_property(object, name, &z_copy, cache_slot);
OBJ_RELEASE(object);
zval_ptr_dtor(&z_copy);
zval_ptr_dtor(z);
if (z == &rv) {
zval_ptr_dtor(z);
}
}
static zend_never_inline void zend_pre_incdec_overloaded_property(zend_object *object, zend_string *name, void **cache_slot OPLINE_DC EXECUTE_DATA_DC)
@ -1915,7 +1928,9 @@ static zend_never_inline void zend_pre_incdec_overloaded_property(zend_object *o
object->handlers->write_property(object, name, &z_copy, cache_slot);
OBJ_RELEASE(object);
zval_ptr_dtor(&z_copy);
zval_ptr_dtor(z);
if (z == &rv) {
zval_ptr_dtor(z);
}
}
static zend_never_inline void zend_assign_op_overloaded_property(zend_object *object, zend_string *name, void **cache_slot, zval *value OPLINE_DC EXECUTE_DATA_DC)
@ -1938,7 +1953,9 @@ static zend_never_inline void zend_assign_op_overloaded_property(zend_object *ob
if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
ZVAL_COPY(EX_VAR(opline->result.var), &res);
}
zval_ptr_dtor(z);
if (z == &rv) {
zval_ptr_dtor(z);
}
zval_ptr_dtor(&res);
OBJ_RELEASE(object);
}
@ -2821,9 +2838,22 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c
ptr = OBJ_PROP(zobj, prop_offset);
if (EXPECTED(Z_TYPE_P(ptr) != IS_UNDEF)) {
ZVAL_INDIRECT(result, ptr);
if (flags) {
zend_property_info *prop_info = CACHED_PTR_EX(cache_slot + 2);
if (prop_info) {
zend_property_info *prop_info = CACHED_PTR_EX(cache_slot + 2);
if (prop_info) {
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
/* For objects, R/RW/UNSET fetch modes might not actually modify object.
* Similar as with magic __get() allow them, but return the value as a copy
* to make sure no actual modification is possible. */
ZEND_ASSERT(type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET);
if (Z_TYPE_P(ptr) == IS_OBJECT) {
ZVAL_COPY(result, ptr);
} else {
zend_readonly_property_modification_error(prop_info);
ZVAL_ERROR(result);
}
return;
}
if (flags) {
zend_handle_fetch_obj_flags(result, ptr, NULL, prop_info, flags);
}
}

View File

@ -69,6 +69,8 @@ ZEND_API ZEND_COLD void zend_throw_ref_type_error_type(zend_property_info *prop1
ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_offset_write(HashTable *ht, zend_long lval);
ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht, zend_string *offset);
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_modification_error(zend_property_info *info);
ZEND_API bool zend_verify_scalar_type_hint(uint32_t type_mask, zval *arg, bool strict, bool is_internal_arg);
ZEND_API ZEND_COLD void zend_verify_arg_error(
const zend_function *zf, const zend_arg_info *arg_info, uint32_t arg_num, zval *value);

View File

@ -1249,6 +1249,14 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke
(parent_info->flags & ZEND_ACC_STATIC) ? "static " : "non static ", ZSTR_VAL(parent_info->ce->name), ZSTR_VAL(key),
(child_info->flags & ZEND_ACC_STATIC) ? "static " : "non static ", ZSTR_VAL(ce->name), ZSTR_VAL(key));
}
if (UNEXPECTED((child_info->flags & ZEND_ACC_READONLY) != (parent_info->flags & ZEND_ACC_READONLY))) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot redeclare %s property %s::$%s as %s %s::$%s",
parent_info->flags & ZEND_ACC_READONLY ? "readonly" : "non-readonly",
ZSTR_VAL(parent_info->ce->name), ZSTR_VAL(key),
child_info->flags & ZEND_ACC_READONLY ? "readonly" : "non-readonly",
ZSTR_VAL(ce->name), ZSTR_VAL(key));
}
if (UNEXPECTED((child_info->flags & ZEND_ACC_PPP_MASK) > (parent_info->flags & ZEND_ACC_PPP_MASK))) {
zend_error_noreturn(E_COMPILE_ERROR, "Access level to %s::$%s must be %s (as in class %s)%s", ZSTR_VAL(ce->name), ZSTR_VAL(key), zend_visibility_string(parent_info->flags), ZSTR_VAL(parent_info->ce->name), (parent_info->flags&ZEND_ACC_PUBLIC) ? "" : " or weaker");
@ -2218,10 +2226,10 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent
zend_hash_del(&ce->properties_info, prop_name);
flags |= ZEND_ACC_CHANGED;
} else {
uint32_t flags_mask = ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC | ZEND_ACC_READONLY;
not_compatible = 1;
if ((colliding_prop->flags & (ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC))
== (flags & (ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC)) &&
if ((colliding_prop->flags & flags_mask) == (flags & flags_mask) &&
property_types_compatible(property_info, colliding_prop) == INHERITANCE_SUCCESS
) {
/* the flags are identical, thus, the properties may be compatible */

View File

@ -154,6 +154,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%token <ident> T_PRIVATE "'private'"
%token <ident> T_PROTECTED "'protected'"
%token <ident> T_PUBLIC "'public'"
%token <ident> T_READONLY "'readonly'"
%token <ident> T_VAR "'var'"
%token <ident> T_UNSET "'unset'"
%token <ident> T_ISSET "'isset'"
@ -279,7 +280,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%type <ast> enum_declaration_statement enum_backing_type enum_case enum_case_expr
%type <num> returns_ref function fn is_reference is_variadic variable_modifiers
%type <num> method_modifiers non_empty_member_modifiers member_modifier optional_visibility_modifier
%type <num> method_modifiers non_empty_member_modifiers member_modifier
%type <num> optional_property_modifiers property_modifier
%type <num> class_modifiers class_modifier use_type backup_fn_flags
%type <ptr> backup_lex_pos
@ -305,7 +307,7 @@ reserved_non_modifiers:
semi_reserved:
reserved_non_modifiers
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC | T_READONLY
;
ampersand:
@ -769,19 +771,24 @@ attributed_parameter:
| parameter { $$ = $1; }
;
optional_visibility_modifier:
optional_property_modifiers:
%empty { $$ = 0; }
| T_PUBLIC { $$ = ZEND_ACC_PUBLIC; }
| optional_property_modifiers property_modifier
{ $$ = zend_add_member_modifier($1, $2); if (!$$) { YYERROR; } }
property_modifier:
T_PUBLIC { $$ = ZEND_ACC_PUBLIC; }
| T_PROTECTED { $$ = ZEND_ACC_PROTECTED; }
| T_PRIVATE { $$ = ZEND_ACC_PRIVATE; }
| T_READONLY { $$ = ZEND_ACC_READONLY; }
;
parameter:
optional_visibility_modifier optional_type_without_static
optional_property_modifiers optional_type_without_static
is_reference is_variadic T_VARIABLE backup_doc_comment
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, NULL,
NULL, $6 ? zend_ast_create_zval_from_str($6) : NULL); }
| optional_visibility_modifier optional_type_without_static
| optional_property_modifiers optional_type_without_static
is_reference is_variadic T_VARIABLE backup_doc_comment '=' expr
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, $8,
NULL, $6 ? zend_ast_create_zval_from_str($6) : NULL); }
@ -1001,6 +1008,7 @@ member_modifier:
| T_STATIC { $$ = ZEND_ACC_STATIC; }
| T_ABSTRACT { $$ = ZEND_ACC_ABSTRACT; }
| T_FINAL { $$ = ZEND_ACC_FINAL; }
| T_READONLY { $$ = ZEND_ACC_READONLY; }
;
property_list:

View File

@ -1714,6 +1714,10 @@ NEWLINE ("\r"|"\n"|"\r\n")
RETURN_TOKEN_WITH_IDENT(T_PUBLIC);
}
<ST_IN_SCRIPTING>"readonly" {
RETURN_TOKEN_WITH_IDENT(T_READONLY);
}
<ST_IN_SCRIPTING>"unset" {
RETURN_TOKEN_WITH_IDENT(T_UNSET);
}

View File

@ -277,6 +277,19 @@ static ZEND_COLD zend_never_inline void zend_forbidden_dynamic_property(
ZSTR_VAL(ce->name), ZSTR_VAL(member));
}
static ZEND_COLD zend_never_inline void zend_readonly_property_modification_scope_error(
zend_class_entry *ce, zend_string *member, zend_class_entry *scope, const char *operation) {
zend_throw_error(NULL, "Cannot %s readonly property %s::$%s from %s%s",
operation, ZSTR_VAL(ce->name), ZSTR_VAL(member),
scope ? "scope " : "global scope", scope ? ZSTR_VAL(scope->name) : "");
}
static ZEND_COLD zend_never_inline void zend_readonly_property_unset_error(
zend_class_entry *ce, zend_string *member) {
zend_throw_error(NULL, "Cannot unset readonly property %s::$%s",
ZSTR_VAL(ce->name), ZSTR_VAL(member));
}
static zend_always_inline uintptr_t zend_get_property_offset(zend_class_entry *ce, zend_string *member, int silent, void **cache_slot, zend_property_info **info_ptr) /* {{{ */
{
zval *zv;
@ -573,6 +586,19 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) {
retval = OBJ_PROP(zobj, property_offset);
if (EXPECTED(Z_TYPE_P(retval) != IS_UNDEF)) {
if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)
&& (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET)) {
if (Z_TYPE_P(retval) == IS_OBJECT) {
/* For objects, R/RW/UNSET fetch modes might not actually modify object.
* Similar as with magic __get() allow them, but return the value as a copy
* to make sure no actual modification is possible. */
ZVAL_COPY(rv, retval);
retval = rv;
} else {
zend_readonly_property_modification_error(prop_info);
retval = &EG(uninitialized_zval);
}
}
goto exit;
}
if (UNEXPECTED(Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
@ -708,6 +734,36 @@ static zend_always_inline bool property_uses_strict_types() {
&& ZEND_CALL_USES_STRICT_TYPES(EG(current_execute_data));
}
static bool verify_readonly_initialization_access(
zend_property_info *prop_info, zend_class_entry *ce,
zend_string *name, const char *operation) {
zend_class_entry *scope;
if (UNEXPECTED(EG(fake_scope))) {
scope = EG(fake_scope);
} else {
scope = zend_get_executed_scope();
}
if (prop_info->ce == scope) {
return true;
}
/* We may have redeclared a parent property. In that case the parent should still be
* allowed to initialize it. */
if (scope && is_derived_class(ce, scope)) {
zend_property_info *prop_info = zend_hash_find_ptr(&scope->properties_info, name);
if (prop_info) {
/* This should be ensured by inheritance. */
ZEND_ASSERT(prop_info->flags & ZEND_ACC_READONLY);
if (prop_info->ce == scope) {
return true;
}
}
}
zend_readonly_property_modification_scope_error(prop_info->ce, name, scope, operation);
return false;
}
ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zval *value, void **cache_slot) /* {{{ */
{
zval *variable_ptr, tmp;
@ -723,6 +779,13 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
Z_TRY_ADDREF_P(value);
if (UNEXPECTED(prop_info)) {
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
Z_TRY_DELREF_P(value);
zend_readonly_property_modification_error(prop_info);
variable_ptr = &EG(error_zval);
goto exit;
}
ZVAL_COPY_VALUE(&tmp, value);
if (UNEXPECTED(!zend_verify_property_type(prop_info, &tmp, property_uses_strict_types()))) {
Z_TRY_DELREF_P(value);
@ -787,6 +850,13 @@ write_std_property:
Z_TRY_ADDREF_P(value);
if (UNEXPECTED(prop_info)) {
if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY)
&& !verify_readonly_initialization_access(prop_info, zobj->ce, name, "initialize"))) {
Z_TRY_DELREF_P(value);
variable_ptr = &EG(error_zval);
goto exit;
}
ZVAL_COPY_VALUE(&tmp, value);
if (UNEXPECTED(!zend_verify_property_type(prop_info, &tmp, property_uses_strict_types()))) {
zval_ptr_dtor(value);
@ -955,6 +1025,9 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
/* we do have getter - fail and let it try again with usual get/set */
retval = NULL;
}
} else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
/* Readonly property, delegate to read_property + write_property. */
retval = NULL;
}
} else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset))) {
if (EXPECTED(zobj->properties)) {
@ -1003,6 +1076,10 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
zval *slot = OBJ_PROP(zobj, property_offset);
if (Z_TYPE_P(slot) != IS_UNDEF) {
if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) {
zend_readonly_property_unset_error(prop_info->ce, name);
return;
}
if (UNEXPECTED(Z_ISREF_P(slot)) &&
(ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(slot)))) {
if (prop_info) {
@ -1019,6 +1096,11 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
return;
}
if (UNEXPECTED(Z_PROP_FLAG_P(slot) == IS_PROP_UNINIT)) {
if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY)
&& !verify_readonly_initialization_access(prop_info, zobj->ce, name, "unset"))) {
return;
}
/* Reset the IS_PROP_UNINIT flag, if it exists and bypass __unset(). */
Z_PROP_FLAG_P(slot) = 0;
return;

View File

@ -6926,11 +6926,20 @@ ZEND_VM_HANDLER(126, ZEND_FE_FETCH_RW, VAR, ANY, JMP_ADDR)
&& EXPECTED(zend_check_property_access(Z_OBJ_P(array), p->key, 0) == SUCCESS)) {
if ((value_type & Z_TYPE_MASK) != IS_REFERENCE) {
zend_property_info *prop_info =
zend_get_typed_property_info_for_slot(Z_OBJ_P(array), value);
zend_get_property_info_for_slot(Z_OBJ_P(array), value);
if (UNEXPECTED(prop_info)) {
ZVAL_NEW_REF(value, value);
ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(value), prop_info);
value_type = IS_REFERENCE_EX;
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
zend_throw_error(NULL,
"Cannot acquire reference to readonly property %s::$%s",
ZSTR_VAL(prop_info->ce->name), ZSTR_VAL(p->key));
UNDEF_RESULT();
HANDLE_EXCEPTION();
}
if (ZEND_TYPE_IS_SET(prop_info->type)) {
ZVAL_NEW_REF(value, value);
ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(value), prop_info);
value_type = IS_REFERENCE_EX;
}
}
}
break;

View File

@ -21952,11 +21952,20 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_FETCH_RW_SPEC_VAR_HANDLER(Z
&& EXPECTED(zend_check_property_access(Z_OBJ_P(array), p->key, 0) == SUCCESS)) {
if ((value_type & Z_TYPE_MASK) != IS_REFERENCE) {
zend_property_info *prop_info =
zend_get_typed_property_info_for_slot(Z_OBJ_P(array), value);
zend_get_property_info_for_slot(Z_OBJ_P(array), value);
if (UNEXPECTED(prop_info)) {
ZVAL_NEW_REF(value, value);
ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(value), prop_info);
value_type = IS_REFERENCE_EX;
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
zend_throw_error(NULL,
"Cannot acquire reference to readonly property %s::$%s",
ZSTR_VAL(prop_info->ce->name), ZSTR_VAL(p->key));
UNDEF_RESULT();
HANDLE_EXCEPTION();
}
if (ZEND_TYPE_IS_SET(prop_info->type)) {
ZVAL_NEW_REF(value, value);
ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(value), prop_info);
value_type = IS_REFERENCE_EX;
}
}
}
break;

View File

@ -11830,7 +11830,6 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
| mov FCARG1x, TMP1
prop_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FCARG1x, 0);
if (opline->opcode == ZEND_FETCH_OBJ_W
&& (opline->extended_value & ZEND_FETCH_OBJ_FLAGS)
&& (!ce || ce_is_instanceof || (ce->ce_flags & ZEND_ACC_HAS_TYPE_HINTS))) {
uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS;
@ -11839,6 +11838,22 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
| cbnz FCARG2x, >1
|.cold_code
|1:
| ldr TMP1w, [FCARG2x, #offsetof(zend_property_info, flags)]
| tst TMP1w, #ZEND_ACC_READONLY
| beq >3
| IF_NOT_TYPE REG2w, IS_OBJECT_EX, >2
| GET_Z_PTR REG2, FCARG1x
| GC_ADDREF REG2, TMP1w
| SET_ZVAL_PTR res_addr, REG2, TMP1
| SET_ZVAL_TYPE_INFO res_addr, IS_OBJECT_EX, TMP1w, TMP2
| b >9
|2:
| mov FCARG1x, FCARG2x
| SET_EX_OPLINE opline, REG0
| EXT_CALL zend_readonly_property_modification_error, REG0
| SET_ZVAL_TYPE_INFO res_addr, _IS_ERROR, TMP1w, TMP2
| b >9
|3:
if (flags == ZEND_FETCH_DIM_WRITE) {
| SET_EX_OPLINE opline, REG0
| EXT_CALL zend_jit_check_array_promotion, REG0
@ -11848,7 +11863,7 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
| EXT_CALL zend_jit_create_typed_ref, REG0
| b >9
} else {
ZEND_UNREACHABLE();
ZEND_ASSERT(flags == 0);
}
|.code
}
@ -11869,6 +11884,22 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
} else {
| IF_UNDEF REG2w, >5
}
if (opline->opcode == ZEND_FETCH_OBJ_W && (prop_info->flags & ZEND_ACC_READONLY)) {
| IF_NOT_TYPE REG2w, IS_OBJECT_EX, >4
| GET_ZVAL_PTR REG2, prop_addr, TMP1
| GC_ADDREF REG2, TMP1w
| SET_ZVAL_PTR res_addr, REG2, TMP1
| SET_ZVAL_TYPE_INFO res_addr, IS_OBJECT_EX, TMP1w, TMP2
| b >9
|.cold_code
|4:
| LOAD_ADDR FCARG1x, prop_info
| SET_EX_OPLINE opline, REG0
| EXT_CALL zend_readonly_property_modification_error, REG0
| SET_ZVAL_TYPE_INFO res_addr, _IS_ERROR, TMP1w, TMP2
| b >9
|.code
}
if (opline->opcode == ZEND_FETCH_OBJ_W
&& (opline->extended_value & ZEND_FETCH_OBJ_FLAGS)
&& ZEND_TYPE_IS_SET(prop_info->type)) {
@ -12895,7 +12926,7 @@ static int zend_jit_assign_obj(dasm_State **Dst,
}
} else {
prop_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FCARG1x, prop_info->offset);
if (!ce || ce_is_instanceof || !(ce->ce_flags & ZEND_ACC_IMMUTABLE) || ce->__get || ce->__set) {
if (!ce || ce_is_instanceof || !(ce->ce_flags & ZEND_ACC_IMMUTABLE) || ce->__get || ce->__set || (prop_info->flags & ZEND_ACC_READONLY)) {
// Undefined property with magic __get()/__set()
if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE) {
int32_t exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM);

View File

@ -2006,6 +2006,14 @@ static void ZEND_FASTCALL zend_jit_assign_to_typed_prop(zval *property_val, zend
zend_execute_data *execute_data = EG(current_execute_data);
zval tmp;
if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
zend_readonly_property_modification_error(info);
if (result) {
ZVAL_UNDEF(result);
}
return;
}
ZVAL_DEREF(value);
ZVAL_COPY(&tmp, value);

View File

@ -12511,7 +12511,6 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
| add FCARG1a, r0
prop_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FCARG1a, 0);
if (opline->opcode == ZEND_FETCH_OBJ_W
&& (opline->extended_value & ZEND_FETCH_OBJ_FLAGS)
&& (!ce || ce_is_instanceof || (ce->ce_flags & ZEND_ACC_HAS_TYPE_HINTS))) {
uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS;
@ -12521,6 +12520,21 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
| jnz >1
|.cold_code
|1:
| test dword [FCARG2a + offsetof(zend_property_info, flags)], ZEND_ACC_READONLY
| jz >3
| IF_NOT_Z_TYPE FCARG1a, IS_OBJECT, >2
| GET_Z_PTR r0, FCARG1a
| GC_ADDREF r0
| SET_ZVAL_PTR res_addr, r0
| SET_ZVAL_TYPE_INFO res_addr, IS_OBJECT_EX
| jmp >9
|2:
| mov FCARG1a, FCARG2a
| SET_EX_OPLINE opline, r0
| EXT_CALL zend_readonly_property_modification_error, r0
| SET_ZVAL_TYPE_INFO res_addr, _IS_ERROR
| jmp >9
|3:
if (flags == ZEND_FETCH_DIM_WRITE) {
| SET_EX_OPLINE opline, r0
| EXT_CALL zend_jit_check_array_promotion, r0
@ -12538,7 +12552,7 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
|.endif
| jmp >9
} else {
ZEND_UNREACHABLE();
ZEND_ASSERT(flags == 0);
}
|.code
}
@ -12559,6 +12573,22 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
} else {
| IF_UNDEF dl, >5
}
if (opline->opcode == ZEND_FETCH_OBJ_W && (prop_info->flags & ZEND_ACC_READONLY)) {
| IF_NOT_TYPE dl, IS_OBJECT, >4
| GET_ZVAL_PTR r0, prop_addr
| GC_ADDREF r0
| SET_ZVAL_PTR res_addr, r0
| SET_ZVAL_TYPE_INFO res_addr, IS_OBJECT_EX
| jmp >9
|.cold_code
|4:
| LOAD_ADDR FCARG1a, prop_info
| SET_EX_OPLINE opline, r0
| EXT_CALL zend_readonly_property_modification_error, r0
| SET_ZVAL_TYPE_INFO res_addr, _IS_ERROR
| jmp >9
|.code
}
if (opline->opcode == ZEND_FETCH_OBJ_W
&& (opline->extended_value & ZEND_FETCH_OBJ_FLAGS)
&& ZEND_TYPE_IS_SET(prop_info->type)) {
@ -13677,7 +13707,7 @@ static int zend_jit_assign_obj(dasm_State **Dst,
}
} else {
prop_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FCARG1a, prop_info->offset);
if (!ce || ce_is_instanceof || !(ce->ce_flags & ZEND_ACC_IMMUTABLE) || ce->__get || ce->__set) {
if (!ce || ce_is_instanceof || !(ce->ce_flags & ZEND_ACC_IMMUTABLE) || ce->__get || ce->__set || (prop_info->flags & ZEND_ACC_READONLY)) {
// Undefined property with magic __get()/__set()
if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE) {
int32_t exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM);

View File

@ -5509,6 +5509,11 @@ ZEND_METHOD(ReflectionProperty, isStatic)
}
/* }}} */
ZEND_METHOD(ReflectionProperty, isReadOnly)
{
_property_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_READONLY);
}
/* {{{ Returns whether this property is default (declared at compilation time). */
ZEND_METHOD(ReflectionProperty, isDefault)
{
@ -5535,7 +5540,7 @@ ZEND_METHOD(ReflectionProperty, getModifiers)
{
reflection_object *intern;
property_reference *ref;
uint32_t keep_flags = ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC;
uint32_t keep_flags = ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC | ZEND_ACC_READONLY;
if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
@ -7102,6 +7107,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */
reflection_property_ptr->create_object = reflection_objects_new;
REGISTER_REFLECTION_CLASS_CONST_LONG(property, "IS_STATIC", ZEND_ACC_STATIC);
REGISTER_REFLECTION_CLASS_CONST_LONG(property, "IS_READONLY", ZEND_ACC_READONLY);
REGISTER_REFLECTION_CLASS_CONST_LONG(property, "IS_PUBLIC", ZEND_ACC_PUBLIC);
REGISTER_REFLECTION_CLASS_CONST_LONG(property, "IS_PROTECTED", ZEND_ACC_PROTECTED);
REGISTER_REFLECTION_CLASS_CONST_LONG(property, "IS_PRIVATE", ZEND_ACC_PRIVATE);

View File

@ -428,6 +428,8 @@ class ReflectionProperty implements Reflector
/** @tentative-return-type */
public function isStatic(): bool {}
public function isReadOnly(): bool {}
/** @tentative-return-type */
public function isDefault(): bool {}

View File

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 49f7b49b187721a143fc54ea1eb4414805603dba */
* Stub hash: 74eb6f065b43ebd0ccff2510a49ac5c6fe716910 */
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)
@ -354,6 +354,8 @@ ZEND_END_ARG_INFO()
#define arginfo_class_ReflectionProperty_isStatic arginfo_class_ReflectionFunctionAbstract_inNamespace
#define arginfo_class_ReflectionProperty_isReadOnly arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType
#define arginfo_class_ReflectionProperty_isDefault arginfo_class_ReflectionFunctionAbstract_inNamespace
#define arginfo_class_ReflectionProperty_isPromoted arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType
@ -722,6 +724,7 @@ ZEND_METHOD(ReflectionProperty, isPublic);
ZEND_METHOD(ReflectionProperty, isPrivate);
ZEND_METHOD(ReflectionProperty, isProtected);
ZEND_METHOD(ReflectionProperty, isStatic);
ZEND_METHOD(ReflectionProperty, isReadOnly);
ZEND_METHOD(ReflectionProperty, isDefault);
ZEND_METHOD(ReflectionProperty, isPromoted);
ZEND_METHOD(ReflectionProperty, getModifiers);
@ -997,6 +1000,7 @@ static const zend_function_entry class_ReflectionProperty_methods[] = {
ZEND_ME(ReflectionProperty, isPrivate, arginfo_class_ReflectionProperty_isPrivate, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionProperty, isProtected, arginfo_class_ReflectionProperty_isProtected, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionProperty, isStatic, arginfo_class_ReflectionProperty_isStatic, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionProperty, isReadOnly, arginfo_class_ReflectionProperty_isReadOnly, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionProperty, isDefault, arginfo_class_ReflectionProperty_isDefault, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionProperty, isPromoted, arginfo_class_ReflectionProperty_isPromoted, ZEND_ACC_PUBLIC)
ZEND_ME(ReflectionProperty, getModifiers, arginfo_class_ReflectionProperty_getModifiers, ZEND_ACC_PUBLIC)

View File

@ -0,0 +1,24 @@
--TEST--
Readonly property reflection
--FILE--
<?php
class Test {
public int $rw;
public readonly int $ro;
}
$rp = new ReflectionProperty(Test::class, 'rw');
var_dump($rp->isReadOnly());
var_dump(($rp->getModifiers() & ReflectionProperty::IS_READONLY) != 0);
$rp = new ReflectionProperty(Test::class, 'ro');
var_dump($rp->isReadOnly());
var_dump(($rp->getModifiers() & ReflectionProperty::IS_READONLY) != 0);
?>
--EXPECT--
bool(false)
bool(false)
bool(true)
bool(true)

View File

@ -92,6 +92,7 @@ void tokenizer_register_constants(INIT_FUNC_ARGS) {
REGISTER_LONG_CONSTANT("T_PRIVATE", T_PRIVATE, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_PROTECTED", T_PROTECTED, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_PUBLIC", T_PUBLIC, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_READONLY", T_READONLY, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_VAR", T_VAR, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_UNSET", T_UNSET, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("T_ISSET", T_ISSET, CONST_CS | CONST_PERSISTENT);
@ -244,6 +245,7 @@ char *get_token_type_name(int token_type)
case T_PRIVATE: return "T_PRIVATE";
case T_PROTECTED: return "T_PROTECTED";
case T_PUBLIC: return "T_PUBLIC";
case T_READONLY: return "T_READONLY";
case T_VAR: return "T_VAR";
case T_UNSET: return "T_UNSET";
case T_ISSET: return "T_ISSET";