Skip to content

Commit e4f727d

Browse files
committed
Implement ReflectionProperty::is{Readable,Writable}()
RFC: https://wiki.php.net/rfc/isreadable-iswriteable Fixes GH-15309 Fixes GH-16175 Closes GH-16209
1 parent 7923dc2 commit e4f727d

23 files changed

+942
-5
lines changed

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ PHP 8.6 UPGRADE NOTES
134134

135135
- Reflection:
136136
. ReflectionConstant::inNamespace()
137+
. ReflectionProperty::isReadable() ReflectionProperty::isWritable() were
138+
added.
137139

138140
- Standard:
139141
. `clamp()` returns the given value if in range, else return the nearest

ext/reflection/php_reflection.c

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6601,6 +6601,242 @@ ZEND_METHOD(ReflectionProperty, isFinal)
66016601
_property_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_FINAL);
66026602
}
66036603

6604+
static zend_result get_ce_from_scope_name(zend_class_entry **scope, zend_string *scope_name, zend_execute_data *execute_data)
6605+
{
6606+
if (!scope_name) {
6607+
*scope = NULL;
6608+
return SUCCESS;
6609+
}
6610+
6611+
*scope = zend_lookup_class(scope_name);
6612+
if (!*scope) {
6613+
zend_throw_error(NULL, "Class \"%s\" not found", ZSTR_VAL(scope_name));
6614+
return FAILURE;
6615+
}
6616+
return SUCCESS;
6617+
}
6618+
6619+
static zend_always_inline uint32_t set_visibility_to_visibility(uint32_t set_visibility)
6620+
{
6621+
switch (set_visibility) {
6622+
case ZEND_ACC_PUBLIC_SET:
6623+
return ZEND_ACC_PUBLIC;
6624+
case ZEND_ACC_PROTECTED_SET:
6625+
return ZEND_ACC_PROTECTED;
6626+
case ZEND_ACC_PRIVATE_SET:
6627+
return ZEND_ACC_PRIVATE;
6628+
EMPTY_SWITCH_DEFAULT_CASE();
6629+
}
6630+
}
6631+
6632+
static bool check_visibility(uint32_t visibility, zend_class_entry *ce, zend_class_entry *scope)
6633+
{
6634+
if (!(visibility & ZEND_ACC_PUBLIC) && (scope != ce)) {
6635+
if (!scope) {
6636+
return false;
6637+
}
6638+
if (visibility & ZEND_ACC_PRIVATE) {
6639+
return false;
6640+
}
6641+
ZEND_ASSERT(visibility & ZEND_ACC_PROTECTED);
6642+
if (!instanceof_function(scope, ce) && !instanceof_function(ce, scope)) {
6643+
return false;
6644+
}
6645+
}
6646+
return true;
6647+
}
6648+
6649+
ZEND_METHOD(ReflectionProperty, isReadable)
6650+
{
6651+
reflection_object *intern;
6652+
property_reference *ref;
6653+
zend_string *scope_name;
6654+
zend_object *obj = NULL;
6655+
6656+
ZEND_PARSE_PARAMETERS_START(1, 2)
6657+
Z_PARAM_STR_OR_NULL(scope_name)
6658+
Z_PARAM_OPTIONAL
6659+
Z_PARAM_OBJ_OR_NULL(obj)
6660+
ZEND_PARSE_PARAMETERS_END();
6661+
6662+
GET_REFLECTION_OBJECT_PTR(ref);
6663+
6664+
zend_property_info *prop = ref->prop;
6665+
if (prop && obj) {
6666+
if (prop->flags & ZEND_ACC_STATIC) {
6667+
_DO_THROW("null is expected as object argument for static properties");
6668+
RETURN_THROWS();
6669+
}
6670+
if (!instanceof_function(obj->ce, prop->ce)) {
6671+
_DO_THROW("Given object is not an instance of the class this property was declared in");
6672+
RETURN_THROWS();
6673+
}
6674+
prop = reflection_property_get_effective_prop(ref, intern->ce, obj);
6675+
}
6676+
6677+
zend_class_entry *ce = obj ? obj->ce : intern->ce;
6678+
if (!prop) {
6679+
if (obj && obj->properties && zend_hash_find_ptr(obj->properties, ref->unmangled_name)) {
6680+
RETURN_TRUE;
6681+
}
6682+
handle_magic_get:
6683+
if (ce->__get) {
6684+
if (obj && ce->__isset) {
6685+
uint32_t *guard = zend_get_property_guard(obj, ref->unmangled_name);
6686+
if (!((*guard) & ZEND_GUARD_PROPERTY_ISSET)) {
6687+
GC_ADDREF(obj);
6688+
*guard |= ZEND_GUARD_PROPERTY_ISSET;
6689+
zval member;
6690+
ZVAL_STR(&member, ref->unmangled_name);
6691+
zend_call_known_instance_method_with_1_params(ce->__isset, obj, return_value, &member);
6692+
*guard &= ~ZEND_GUARD_PROPERTY_ISSET;
6693+
OBJ_RELEASE(obj);
6694+
return;
6695+
}
6696+
}
6697+
RETURN_TRUE;
6698+
}
6699+
if (obj && zend_lazy_object_must_init(obj)) {
6700+
obj = zend_lazy_object_init(obj);
6701+
if (!obj) {
6702+
RETURN_THROWS();
6703+
}
6704+
if (obj->properties && zend_hash_find_ptr(obj->properties, ref->unmangled_name)) {
6705+
RETURN_TRUE;
6706+
}
6707+
}
6708+
RETURN_FALSE;
6709+
}
6710+
6711+
zend_class_entry *scope;
6712+
if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) {
6713+
RETURN_THROWS();
6714+
}
6715+
6716+
if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) {
6717+
if (!(prop->flags & ZEND_ACC_STATIC)) {
6718+
goto handle_magic_get;
6719+
}
6720+
RETURN_FALSE;
6721+
}
6722+
6723+
if (prop->flags & ZEND_ACC_VIRTUAL) {
6724+
ZEND_ASSERT(prop->hooks);
6725+
if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) {
6726+
RETURN_FALSE;
6727+
}
6728+
} else if (obj && (!prop->hooks || !prop->hooks[ZEND_PROPERTY_HOOK_GET])) {
6729+
retry_declared:;
6730+
zval *prop_val = OBJ_PROP(obj, prop->offset);
6731+
if (Z_TYPE_P(prop_val) == IS_UNDEF) {
6732+
if (zend_lazy_object_must_init(obj) && (Z_PROP_FLAG_P(prop_val) & IS_PROP_LAZY)) {
6733+
obj = zend_lazy_object_init(obj);
6734+
if (!obj) {
6735+
RETURN_THROWS();
6736+
}
6737+
goto retry_declared;
6738+
}
6739+
if (!(Z_PROP_FLAG_P(prop_val) & IS_PROP_UNINIT)) {
6740+
goto handle_magic_get;
6741+
}
6742+
RETURN_FALSE;
6743+
}
6744+
} else if (prop->flags & ZEND_ACC_STATIC) {
6745+
if (ce->default_static_members_count && !CE_STATIC_MEMBERS(ce)) {
6746+
zend_class_init_statics(ce);
6747+
}
6748+
zval *prop_val = CE_STATIC_MEMBERS(ce) + prop->offset;
6749+
RETURN_BOOL(!Z_ISUNDEF_P(prop_val));
6750+
}
6751+
6752+
RETURN_TRUE;
6753+
}
6754+
6755+
ZEND_METHOD(ReflectionProperty, isWritable)
6756+
{
6757+
reflection_object *intern;
6758+
property_reference *ref;
6759+
zend_string *scope_name;
6760+
zend_object *obj = NULL;
6761+
6762+
ZEND_PARSE_PARAMETERS_START(1, 2)
6763+
Z_PARAM_STR_OR_NULL(scope_name)
6764+
Z_PARAM_OPTIONAL
6765+
Z_PARAM_OBJ_OR_NULL(obj)
6766+
ZEND_PARSE_PARAMETERS_END();
6767+
6768+
GET_REFLECTION_OBJECT_PTR(ref);
6769+
6770+
zend_property_info *prop = ref->prop;
6771+
if (prop && obj) {
6772+
if (prop->flags & ZEND_ACC_STATIC) {
6773+
_DO_THROW("null is expected as object argument for static properties");
6774+
RETURN_THROWS();
6775+
}
6776+
if (!instanceof_function(obj->ce, prop->ce)) {
6777+
_DO_THROW("Given object is not an instance of the class this property was declared in");
6778+
RETURN_THROWS();
6779+
}
6780+
prop = reflection_property_get_effective_prop(ref, intern->ce, obj);
6781+
}
6782+
6783+
zend_class_entry *ce = obj ? obj->ce : intern->ce;
6784+
if (!prop) {
6785+
if (!(ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) {
6786+
RETURN_TRUE;
6787+
}
6788+
/* This path is effectively unreachable, but theoretically possible for
6789+
* two internal classes where ZEND_ACC_NO_DYNAMIC_PROPERTIES is only
6790+
* added to the subclass, in which case a ReflectionProperty can be
6791+
* constructed on the parent class, and then tested on the subclass. */
6792+
handle_magic_set:
6793+
RETURN_BOOL(ce->__set);
6794+
}
6795+
6796+
zend_class_entry *scope;
6797+
if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) {
6798+
RETURN_THROWS();
6799+
}
6800+
6801+
if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) {
6802+
if (!(prop->flags & ZEND_ACC_STATIC)) {
6803+
goto handle_magic_set;
6804+
}
6805+
RETURN_FALSE;
6806+
}
6807+
uint32_t set_visibility = prop->flags & ZEND_ACC_PPP_SET_MASK;
6808+
if (!set_visibility) {
6809+
set_visibility = zend_visibility_to_set_visibility(prop->flags & ZEND_ACC_PPP_MASK);
6810+
}
6811+
if (!check_visibility(set_visibility_to_visibility(set_visibility), prop->ce, scope)) {
6812+
RETURN_FALSE;
6813+
}
6814+
6815+
if (prop->flags & ZEND_ACC_VIRTUAL) {
6816+
ZEND_ASSERT(prop->hooks);
6817+
if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) {
6818+
RETURN_FALSE;
6819+
}
6820+
} else if (obj && (prop->flags & ZEND_ACC_READONLY)) {
6821+
retry:;
6822+
zval *prop_val = OBJ_PROP(obj, prop->offset);
6823+
if (Z_TYPE_P(prop_val) == IS_UNDEF
6824+
&& zend_lazy_object_must_init(obj)
6825+
&& (Z_PROP_FLAG_P(prop_val) & IS_PROP_LAZY)) {
6826+
obj = zend_lazy_object_init(obj);
6827+
if (!obj) {
6828+
RETURN_THROWS();
6829+
}
6830+
goto retry;
6831+
}
6832+
if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) {
6833+
RETURN_FALSE;
6834+
}
6835+
}
6836+
6837+
RETURN_TRUE;
6838+
}
6839+
66046840
/* {{{ Constructor. Throws an Exception in case the given extension does not exist */
66056841
ZEND_METHOD(ReflectionExtension, __construct)
66066842
{

ext/reflection/php_reflection.stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,10 @@ public function hasHook(PropertyHookType $type): bool {}
574574
public function getHook(PropertyHookType $type): ?ReflectionMethod {}
575575

576576
public function isFinal(): bool {}
577+
578+
public function isReadable(?string $scope, ?object $object = null): bool {}
579+
580+
public function isWritable(?string $scope, ?object $object = null): bool {}
577581
}
578582

579583
/** @not-serializable */

ext/reflection/php_reflection_arginfo.h

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/reflection/php_reflection_decl.h

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Test ReflectionProperty::isReadable() dynamic
3+
--FILE--
4+
<?php
5+
6+
#[AllowDynamicProperties]
7+
class A {}
8+
9+
$a = new A;
10+
11+
$a->a = 'a';
12+
$r = new ReflectionProperty($a, 'a');
13+
14+
var_dump($r->isReadable(null, $a));
15+
unset($a->a);
16+
var_dump($r->isReadable(null, $a));
17+
18+
$a = new A;
19+
var_dump($r->isReadable(null, $a));
20+
21+
var_dump($r->isReadable(null, null));
22+
23+
?>
24+
--EXPECT--
25+
bool(true)
26+
bool(false)
27+
bool(false)
28+
bool(false)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Test ReflectionProperty::isReadable() hooks
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public $a { get => $this->a; }
8+
public $b { get => 42; }
9+
public $c { set => $value; }
10+
public $d { set {} }
11+
public $e { get => $this->e; set => $value; }
12+
public $f { get {} set {} }
13+
}
14+
15+
function test($scope) {
16+
$rc = new ReflectionClass(A::class);
17+
foreach ($rc->getProperties() as $rp) {
18+
echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': ';
19+
var_dump($rp->isReadable($scope, null));
20+
}
21+
}
22+
23+
test('A');
24+
test(null);
25+
26+
?>
27+
--EXPECT--
28+
a from A: bool(true)
29+
b from A: bool(true)
30+
c from A: bool(true)
31+
d from A: bool(false)
32+
e from A: bool(true)
33+
f from A: bool(true)
34+
a from global: bool(true)
35+
b from global: bool(true)
36+
c from global: bool(true)
37+
d from global: bool(false)
38+
e from global: bool(true)
39+
f from global: bool(true)

0 commit comments

Comments
 (0)