Skip to content

Commit 325b8fa

Browse files
committed
Implement ReflectionProperty::is{Readable,Writable}()
Fixes GH-15309 Fixes GH-16175
1 parent 984f95f commit 325b8fa

18 files changed

+797
-5
lines changed

ext/reflection/php_reflection.c

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6601,6 +6601,215 @@ 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+
RETURN_FALSE;
6700+
}
6701+
6702+
zend_class_entry *scope;
6703+
if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) {
6704+
RETURN_THROWS();
6705+
}
6706+
6707+
if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) {
6708+
if (!(prop->flags & ZEND_ACC_STATIC)) {
6709+
goto handle_magic_get;
6710+
}
6711+
RETURN_FALSE;
6712+
}
6713+
6714+
if (prop->flags & ZEND_ACC_VIRTUAL) {
6715+
ZEND_ASSERT(prop->hooks);
6716+
if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) {
6717+
RETURN_FALSE;
6718+
}
6719+
} else if (obj && (!prop->hooks || !prop->hooks[ZEND_PROPERTY_HOOK_GET])) {
6720+
zval *prop_val = OBJ_PROP(obj, prop->offset);
6721+
if (Z_TYPE_P(prop_val) == IS_UNDEF) {
6722+
if (!(Z_PROP_FLAG_P(prop_val) & IS_PROP_UNINIT)) {
6723+
goto handle_magic_get;
6724+
}
6725+
RETURN_FALSE;
6726+
}
6727+
} else if (prop->flags & ZEND_ACC_STATIC) {
6728+
if (ce->default_static_members_count && !CE_STATIC_MEMBERS(ce)) {
6729+
zend_class_init_statics(ce);
6730+
}
6731+
zval *prop_val = CE_STATIC_MEMBERS(ce) + prop->offset;
6732+
RETURN_BOOL(!Z_ISUNDEF_P(prop_val));
6733+
}
6734+
6735+
RETURN_TRUE;
6736+
}
6737+
6738+
ZEND_METHOD(ReflectionProperty, isWritable)
6739+
{
6740+
reflection_object *intern;
6741+
property_reference *ref;
6742+
zend_string *scope_name;
6743+
zend_object *obj = NULL;
6744+
6745+
ZEND_PARSE_PARAMETERS_START(1, 2)
6746+
Z_PARAM_STR_OR_NULL(scope_name)
6747+
Z_PARAM_OPTIONAL
6748+
Z_PARAM_OBJ_OR_NULL(obj)
6749+
ZEND_PARSE_PARAMETERS_END();
6750+
6751+
GET_REFLECTION_OBJECT_PTR(ref);
6752+
6753+
zend_property_info *prop = ref->prop;
6754+
if (prop && obj) {
6755+
if (prop->flags & ZEND_ACC_STATIC) {
6756+
_DO_THROW("null is expected as object argument for static properties");
6757+
RETURN_THROWS();
6758+
}
6759+
if (!instanceof_function(obj->ce, prop->ce)) {
6760+
_DO_THROW("Given object is not an instance of the class this property was declared in");
6761+
RETURN_THROWS();
6762+
}
6763+
prop = reflection_property_get_effective_prop(ref, intern->ce, obj);
6764+
}
6765+
6766+
zend_class_entry *ce = obj ? obj->ce : intern->ce;
6767+
if (!prop) {
6768+
if (!(ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) {
6769+
RETURN_TRUE;
6770+
}
6771+
/* This path is effectively unreachable, but theoretically possible for
6772+
* two internal classes where ZEND_ACC_NO_DYNAMIC_PROPERTIES is only
6773+
* added to the subclass, in which case a ReflectionProperty can be
6774+
* constructed on the parent class, and then tested on the subclass. */
6775+
handle_magic_set:
6776+
RETURN_BOOL(ce->__set);
6777+
}
6778+
6779+
zend_class_entry *scope;
6780+
if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) {
6781+
RETURN_THROWS();
6782+
}
6783+
6784+
if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) {
6785+
if (!(prop->flags & ZEND_ACC_STATIC)) {
6786+
goto handle_magic_set;
6787+
}
6788+
RETURN_FALSE;
6789+
}
6790+
uint32_t set_visibility = prop->flags & ZEND_ACC_PPP_SET_MASK;
6791+
if (!set_visibility) {
6792+
set_visibility = zend_visibility_to_set_visibility(prop->flags & ZEND_ACC_PPP_MASK);
6793+
}
6794+
if (!check_visibility(set_visibility_to_visibility(set_visibility), prop->ce, scope)) {
6795+
RETURN_FALSE;
6796+
}
6797+
6798+
if (prop->flags & ZEND_ACC_VIRTUAL) {
6799+
ZEND_ASSERT(prop->hooks);
6800+
if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) {
6801+
RETURN_FALSE;
6802+
}
6803+
} else if (obj && (prop->flags & ZEND_ACC_READONLY)) {
6804+
zval *prop_val = OBJ_PROP(obj, prop->offset);
6805+
if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) {
6806+
RETURN_FALSE;
6807+
}
6808+
}
6809+
6810+
RETURN_TRUE;
6811+
}
6812+
66046813
/* {{{ Constructor. Throws an Exception in case the given extension does not exist */
66056814
ZEND_METHOD(ReflectionExtension, __construct)
66066815
{

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)