From 15c28c26a62f99eceb6b6aacaa45c26474a63a67 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 4 Oct 2024 00:59:24 +0200 Subject: [PATCH 1/7] Implement ReflectionProperty::is{Readable,Writable}() Fixes GH-15309 Fixes GH-16175 --- ext/reflection/php_reflection.c | 162 ++++++++++++++++++ ext/reflection/php_reflection.stub.php | 4 + ext/reflection/php_reflection_arginfo.h | 13 +- .../tests/ReflectionProperty_isReadable.phpt | 84 +++++++++ .../tests/ReflectionProperty_isWritable.phpt | 90 ++++++++++ 5 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index d6e55c982b425..2c7947b1d3ecc 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6633,6 +6633,168 @@ ZEND_METHOD(ReflectionProperty, isFinal) _property_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_FINAL); } +static zend_result get_ce_from_scope_name(zend_class_entry **scope, zend_string *scope_name, zend_execute_data *execute_data) +{ + if (!scope_name) { + *scope = NULL; + return SUCCESS; + } + if (zend_string_equals(scope_name, ZSTR_KNOWN(ZEND_STR_STATIC))) { + *scope = EX(prev_execute_data)->func->common.scope; + return SUCCESS; + } + + *scope = zend_lookup_class(scope_name); + if (!*scope) { + zend_throw_error(NULL, "Class \"%s\" not found", ZSTR_VAL(scope_name)); + return FAILURE; + } + return SUCCESS; +} + +static zend_always_inline uint32_t set_visibility_to_visibility(uint32_t set_visibility) +{ + switch (set_visibility) { + case ZEND_ACC_PUBLIC_SET: + return ZEND_ACC_PUBLIC; + case ZEND_ACC_PROTECTED_SET: + return ZEND_ACC_PROTECTED; + case ZEND_ACC_PRIVATE_SET: + return ZEND_ACC_PRIVATE; + EMPTY_SWITCH_DEFAULT_CASE(); + } +} + +static bool check_visibility(uint32_t visibility, zend_class_entry *ce, zend_class_entry *scope) +{ + if (!(visibility & ZEND_ACC_PUBLIC) && (scope != ce)) { + if (!scope) { + return false; + } + if (visibility & ZEND_ACC_PRIVATE) { + return false; + } + ZEND_ASSERT(visibility & ZEND_ACC_PROTECTED); + if (!instanceof_function(scope, ce) && !instanceof_function(ce, scope)) { + return false; + } + } + return true; +} + +ZEND_METHOD(ReflectionProperty, isReadable) +{ + reflection_object *intern; + property_reference *ref; + zend_object *obj = NULL; + zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC); + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_OBJ_OR_NULL(obj) + Z_PARAM_STR_OR_NULL(scope_name) + ZEND_PARSE_PARAMETERS_END(); + + GET_REFLECTION_OBJECT_PTR(ref); + zend_property_info *prop = ref->prop; + if (!prop) { + _DO_THROW("May not use isReadable on dynamic properties"); + RETURN_THROWS(); + } + + if (obj) { + if (!instanceof_function(obj->ce, prop->ce)) { + _DO_THROW("Given object is not an instance of the class this property was declared in"); + RETURN_THROWS(); + } + prop = reflection_property_get_effective_prop(ref, intern->ce, obj); + } + + zend_class_entry *scope; + if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) { + RETURN_THROWS(); + } + + if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) { + RETURN_FALSE; + } + + if (prop->flags & ZEND_ACC_VIRTUAL) { + ZEND_ASSERT(prop->hooks); + if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) { + RETURN_FALSE; + } + } + + if (obj) { + zval *prop_val = OBJ_PROP(obj, prop->offset); + if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) { + RETURN_FALSE; + } + } + + RETURN_TRUE; +} + +ZEND_METHOD(ReflectionProperty, isWritable) +{ + reflection_object *intern; + property_reference *ref; + zend_object *obj = NULL; + zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC); + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_OBJ_OR_NULL(obj) + Z_PARAM_STR_OR_NULL(scope_name) + ZEND_PARSE_PARAMETERS_END(); + + GET_REFLECTION_OBJECT_PTR(ref); + zend_property_info *prop = ref->prop; + if (!prop) { + _DO_THROW("May not use isWritable on dynamic properties"); + RETURN_THROWS(); + } + + if (obj) { + if (!instanceof_function(obj->ce, prop->ce)) { + _DO_THROW("Given object is not an instance of the class this property was declared in"); + RETURN_THROWS(); + } + prop = reflection_property_get_effective_prop(ref, intern->ce, obj); + } + + zend_class_entry *scope; + if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) { + RETURN_THROWS(); + } + + uint32_t set_visibility = prop->flags & ZEND_ACC_PPP_SET_MASK; + if (!set_visibility) { + set_visibility = zend_visibility_to_set_visibility(prop->flags & ZEND_ACC_PPP_MASK); + } + if (!check_visibility(set_visibility_to_visibility(set_visibility), prop->ce, scope)) { + RETURN_FALSE; + } + + if (prop->flags & ZEND_ACC_VIRTUAL) { + ZEND_ASSERT(prop->hooks); + if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) { + RETURN_FALSE; + } + } + + if (obj && (prop->flags & ZEND_ACC_READONLY)) { + ZEND_ASSERT(prop->offset != ZEND_VIRTUAL_PROPERTY_OFFSET); + zval *prop_val = OBJ_PROP(obj, prop->offset); + if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) { + RETURN_FALSE; + } + } + + RETURN_TRUE; +} + /* {{{ Constructor. Throws an Exception in case the given extension does not exist */ ZEND_METHOD(ReflectionExtension, __construct) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 91c70d6ffdb1a..33a46925a9308 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -571,6 +571,10 @@ public function hasHook(PropertyHookType $type): bool {} public function getHook(PropertyHookType $type): ?ReflectionMethod {} public function isFinal(): bool {} + + public function isReadable(?object $object = null, ?string $scope = 'static'): bool {} + + public function isWritable(?object $object = null, ?string $scope = 'static'): bool {} } /** @not-serializable */ diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 62275423b3caf..55d8482422707 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: fd645a0b0db39d94ca25b39ffe64d7f05bad6bea */ + * Stub hash: f0018fa5d1204c853476bdd34385f3ec85285249 */ 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) @@ -472,6 +472,13 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionProperty_isFinal arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionProperty_isReadable, 0, 0, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, object, IS_OBJECT, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, scope, IS_STRING, 1, "\'static\'") +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionProperty_isWritable arginfo_class_ReflectionProperty_isReadable + #define arginfo_class_ReflectionClassConstant___clone arginfo_class_ReflectionFunctionAbstract___clone ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionClassConstant___construct, 0, 0, 2) @@ -891,6 +898,8 @@ ZEND_METHOD(ReflectionProperty, getHooks); ZEND_METHOD(ReflectionProperty, hasHook); ZEND_METHOD(ReflectionProperty, getHook); ZEND_METHOD(ReflectionProperty, isFinal); +ZEND_METHOD(ReflectionProperty, isReadable); +ZEND_METHOD(ReflectionProperty, isWritable); ZEND_METHOD(ReflectionClassConstant, __construct); ZEND_METHOD(ReflectionClassConstant, __toString); ZEND_METHOD(ReflectionClassConstant, getName); @@ -1194,6 +1203,8 @@ static const zend_function_entry class_ReflectionProperty_methods[] = { ZEND_ME(ReflectionProperty, hasHook, arginfo_class_ReflectionProperty_hasHook, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, getHook, arginfo_class_ReflectionProperty_getHook, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isFinal, arginfo_class_ReflectionProperty_isFinal, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionProperty, isReadable, arginfo_class_ReflectionProperty_isReadable, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionProperty, isWritable, arginfo_class_ReflectionProperty_isWritable, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/reflection/tests/ReflectionProperty_isReadable.phpt b/ext/reflection/tests/ReflectionProperty_isReadable.phpt new file mode 100644 index 0000000000000..83ad52e9b7ee8 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable.phpt @@ -0,0 +1,84 @@ +--TEST-- +Test ReflectionProperty::isReadable() +--FILE-- + 42; } + public $f { set {} } +} + +class C extends B {} + +$test = static function ($scope) { + $rc = new ReflectionClass(B::class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isReadable(null, $scope)); + } +}; + +foreach (['A', 'B', 'C'] as $scope) { + $test($scope); + $test->bindTo(null, $scope)('static'); +} + +$test(null); +$test->bindTo(null, null)('static'); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(false) +d from A: bool(true) +e from A: bool(true) +f from A: bool(false) +a from static: bool(true) +b from static: bool(true) +c from static: bool(false) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) +a from B: bool(true) +b from B: bool(true) +c from B: bool(true) +d from B: bool(true) +e from B: bool(true) +f from B: bool(false) +a from static: bool(true) +b from static: bool(true) +c from static: bool(true) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) +a from C: bool(true) +b from C: bool(true) +c from C: bool(false) +d from C: bool(true) +e from C: bool(true) +f from C: bool(false) +a from static: bool(true) +b from static: bool(true) +c from static: bool(false) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) +a from global: bool(true) +b from global: bool(false) +c from global: bool(false) +d from global: bool(true) +e from global: bool(true) +f from global: bool(false) +a from static: bool(true) +b from static: bool(false) +c from static: bool(false) +d from static: bool(true) +e from static: bool(true) +f from static: bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable.phpt b/ext/reflection/tests/ReflectionProperty_isWritable.phpt new file mode 100644 index 0000000000000..b99782c793875 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable.phpt @@ -0,0 +1,90 @@ +--TEST-- +Test ReflectionProperty::isWritable() +--FILE-- + 42; } + public $f { set {} } + public readonly int $g; + public private(set) int $h; + + public function setG($g) { + $this->g = $g; + } + + public function __clone() { + $rp = new ReflectionProperty(A::class, 'g'); + echo $rp->getName() . ' from static (initialized, clone): '; + var_dump($rp->isWritable($this)); + } +} + +$test = static function ($scope) { + $rc = new ReflectionClass(A::class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isWritable(new A(), $scope)); + + if ($rp->name == 'g') { + $a = new A(); + $a->setG(42); + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ' (initialized): '; + var_dump($rp->isWritable($a, $scope)); + clone $a; + } + } +}; + +$test('A'); +$test->bindTo(null, 'A')('static'); + +$test(null); +$test->bindTo(null, null)('static'); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(true) +d from A: bool(true) +e from A: bool(false) +f from A: bool(true) +g from A: bool(true) +g from A (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from A: bool(true) +a from static: bool(true) +b from static: bool(true) +c from static: bool(true) +d from static: bool(true) +e from static: bool(false) +f from static: bool(true) +g from static: bool(true) +g from static (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from static: bool(true) +a from global: bool(true) +b from global: bool(false) +c from global: bool(false) +d from global: bool(false) +e from global: bool(false) +f from global: bool(true) +g from global: bool(false) +g from global (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from global: bool(false) +a from static: bool(true) +b from static: bool(false) +c from static: bool(false) +d from static: bool(false) +e from static: bool(false) +f from static: bool(true) +g from static: bool(false) +g from static (initialized): bool(false) +g from static (initialized, clone): bool(true) +h from static: bool(false) From b6eb5117f2c1187f5d8e27efb8f5c57329d49534 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 12 Dec 2025 15:27:44 +0100 Subject: [PATCH 2/7] WIP: Handle magic methods and call __isset --- ext/reflection/php_reflection.c | 25 ++++-- .../tests/ReflectionProperty_isReadable.phpt | 84 ----------------- .../ReflectionProperty_isReadable_hooks.phpt | 39 ++++++++ .../ReflectionProperty_isReadable_init.phpt | 72 +++++++++++++++ ...lectionProperty_isReadable_visibility.phpt | 47 ++++++++++ .../tests/ReflectionProperty_isWritable.phpt | 90 ------------------- .../ReflectionProperty_isWritable_hooks.phpt | 39 ++++++++ ...eflectionProperty_isWritable_readonly.phpt | 39 ++++++++ ...lectionProperty_isWritable_visibility.phpt | 57 ++++++++++++ 9 files changed, 311 insertions(+), 181 deletions(-) delete mode 100644 ext/reflection/tests/ReflectionProperty_isReadable.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_init.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt delete mode 100644 ext/reflection/tests/ReflectionProperty_isWritable.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 2c7947b1d3ecc..c1f67b4f5baf0 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6639,10 +6639,6 @@ static zend_result get_ce_from_scope_name(zend_class_entry **scope, zend_string *scope = NULL; return SUCCESS; } - if (zend_string_equals(scope_name, ZSTR_KNOWN(ZEND_STR_STATIC))) { - *scope = EX(prev_execute_data)->func->common.scope; - return SUCCESS; - } *scope = zend_lookup_class(scope_name); if (!*scope) { @@ -6726,10 +6722,25 @@ ZEND_METHOD(ReflectionProperty, isReadable) } } - if (obj) { + if (obj && !prop->hooks) { zval *prop_val = OBJ_PROP(obj, prop->offset); - if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) { - RETURN_FALSE; + if ( Z_TYPE_P(prop_val) == IS_UNDEF) { + if (!obj->ce->__get || (Z_PROP_FLAG_P(prop_val) & IS_PROP_UNINIT)) { + RETURN_FALSE; + } + if (obj->ce->__isset) { + uint32_t *guard = zend_get_property_guard(obj, ref->unmangled_name); + if (!((*guard) & ZEND_GUARD_PROPERTY_ISSET)) { + GC_ADDREF(obj); + *guard |= ZEND_GUARD_PROPERTY_ISSET; + zval member; + ZVAL_STR(&member, ref->unmangled_name); + zend_call_known_instance_method_with_1_params(obj->ce->__isset, obj, return_value, &member); + *guard &= ~ZEND_GUARD_PROPERTY_ISSET; + OBJ_RELEASE(obj); + return; + } + } } } diff --git a/ext/reflection/tests/ReflectionProperty_isReadable.phpt b/ext/reflection/tests/ReflectionProperty_isReadable.phpt deleted file mode 100644 index 83ad52e9b7ee8..0000000000000 --- a/ext/reflection/tests/ReflectionProperty_isReadable.phpt +++ /dev/null @@ -1,84 +0,0 @@ ---TEST-- -Test ReflectionProperty::isReadable() ---FILE-- - 42; } - public $f { set {} } -} - -class C extends B {} - -$test = static function ($scope) { - $rc = new ReflectionClass(B::class); - foreach ($rc->getProperties() as $rp) { - echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isReadable(null, $scope)); - } -}; - -foreach (['A', 'B', 'C'] as $scope) { - $test($scope); - $test->bindTo(null, $scope)('static'); -} - -$test(null); -$test->bindTo(null, null)('static'); - -?> ---EXPECT-- -a from A: bool(true) -b from A: bool(true) -c from A: bool(false) -d from A: bool(true) -e from A: bool(true) -f from A: bool(false) -a from static: bool(true) -b from static: bool(true) -c from static: bool(false) -d from static: bool(true) -e from static: bool(true) -f from static: bool(false) -a from B: bool(true) -b from B: bool(true) -c from B: bool(true) -d from B: bool(true) -e from B: bool(true) -f from B: bool(false) -a from static: bool(true) -b from static: bool(true) -c from static: bool(true) -d from static: bool(true) -e from static: bool(true) -f from static: bool(false) -a from C: bool(true) -b from C: bool(true) -c from C: bool(false) -d from C: bool(true) -e from C: bool(true) -f from C: bool(false) -a from static: bool(true) -b from static: bool(true) -c from static: bool(false) -d from static: bool(true) -e from static: bool(true) -f from static: bool(false) -a from global: bool(true) -b from global: bool(false) -c from global: bool(false) -d from global: bool(true) -e from global: bool(true) -f from global: bool(false) -a from static: bool(true) -b from static: bool(false) -c from static: bool(false) -d from static: bool(true) -e from static: bool(true) -f from static: bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt new file mode 100644 index 0000000000000..ce00d7ac26b1a --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test ReflectionProperty::isReadable() hooks +--FILE-- + $this->a; } + public $b { get => 42; } + public $c { set => $value; } + public $d { set {} } + public $e { get => $this->e; set => $value; } + public $f { get {} set {} } +} + +$test = static function ($scope) { + $rc = new ReflectionClass(A::class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isReadable(null, $scope)); + } +}; + +$test('A'); +$test(null); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(true) +d from A: bool(false) +e from A: bool(true) +f from A: bool(true) +a from global: bool(true) +b from global: bool(true) +c from global: bool(true) +d from global: bool(false) +e from global: bool(true) +f from global: bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt new file mode 100644 index 0000000000000..ca55af75db536 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt @@ -0,0 +1,72 @@ +--TEST-- +Test ReflectionProperty::isReadable() init +--FILE-- +e); + } +} + +class B { + public int $f; + public int $g; + public int $h; + + public function __construct() { + unset($this->g); + unset($this->h); + } + + public function __isset($name) { + return $name === 'h'; + } + + public function __get($name) {} +} + +class C { + public int $i; + public int $j; + public int $k; + + public function __construct() { + unset($this->j); + unset($this->k); + } + + public function __get($name) {} +} + +$test = static function ($class) { + $rc = new ReflectionClass($class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from global: '; + var_dump($rp->isReadable(new $class, NULL)); + } +}; + +$test('A'); +$test('B'); +$test('C'); + +?> +--EXPECT-- +a from global: bool(true) +b from global: bool(false) +c from global: bool(true) +d from global: bool(false) +e from global: bool(false) +f from global: bool(false) +g from global: bool(false) +h from global: bool(true) +i from global: bool(false) +j from global: bool(true) +k from global: bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt new file mode 100644 index 0000000000000..350598c57ffa0 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt @@ -0,0 +1,47 @@ +--TEST-- +Test ReflectionProperty::isReadable() visibility +--FILE-- +getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isReadable(null, $scope)); + } +}; + +foreach (['A', 'B', 'C'] as $scope) { + $test($scope); +} +$test(null); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(false) +d from A: bool(true) +a from B: bool(true) +b from B: bool(true) +c from B: bool(true) +d from B: bool(true) +a from C: bool(true) +b from C: bool(true) +c from C: bool(false) +d from C: bool(true) +a from global: bool(true) +b from global: bool(false) +c from global: bool(false) +d from global: bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable.phpt b/ext/reflection/tests/ReflectionProperty_isWritable.phpt deleted file mode 100644 index b99782c793875..0000000000000 --- a/ext/reflection/tests/ReflectionProperty_isWritable.phpt +++ /dev/null @@ -1,90 +0,0 @@ ---TEST-- -Test ReflectionProperty::isWritable() ---FILE-- - 42; } - public $f { set {} } - public readonly int $g; - public private(set) int $h; - - public function setG($g) { - $this->g = $g; - } - - public function __clone() { - $rp = new ReflectionProperty(A::class, 'g'); - echo $rp->getName() . ' from static (initialized, clone): '; - var_dump($rp->isWritable($this)); - } -} - -$test = static function ($scope) { - $rc = new ReflectionClass(A::class); - foreach ($rc->getProperties() as $rp) { - echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isWritable(new A(), $scope)); - - if ($rp->name == 'g') { - $a = new A(); - $a->setG(42); - echo $rp->getName() . ' from ' . ($scope ?? 'global') . ' (initialized): '; - var_dump($rp->isWritable($a, $scope)); - clone $a; - } - } -}; - -$test('A'); -$test->bindTo(null, 'A')('static'); - -$test(null); -$test->bindTo(null, null)('static'); - -?> ---EXPECT-- -a from A: bool(true) -b from A: bool(true) -c from A: bool(true) -d from A: bool(true) -e from A: bool(false) -f from A: bool(true) -g from A: bool(true) -g from A (initialized): bool(false) -g from static (initialized, clone): bool(true) -h from A: bool(true) -a from static: bool(true) -b from static: bool(true) -c from static: bool(true) -d from static: bool(true) -e from static: bool(false) -f from static: bool(true) -g from static: bool(true) -g from static (initialized): bool(false) -g from static (initialized, clone): bool(true) -h from static: bool(true) -a from global: bool(true) -b from global: bool(false) -c from global: bool(false) -d from global: bool(false) -e from global: bool(false) -f from global: bool(true) -g from global: bool(false) -g from global (initialized): bool(false) -g from static (initialized, clone): bool(true) -h from global: bool(false) -a from static: bool(true) -b from static: bool(false) -c from static: bool(false) -d from static: bool(false) -e from static: bool(false) -f from static: bool(true) -g from static: bool(false) -g from static (initialized): bool(false) -g from static (initialized, clone): bool(true) -h from static: bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt new file mode 100644 index 0000000000000..29c7d11b4e29a --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test ReflectionProperty::isWritable() visibility +--FILE-- + $this->a; } + public $b { get => 42; } + public $c { set => $value; } + public $d { set {} } + public $e { get => $this->e; set => $value; } + public $f { get {} set {} } +} + +$test = static function ($scope) { + $rc = new ReflectionClass(A::class); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isWritable(null, $scope)); + } +}; + +$test('A'); +$test(null); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(false) +c from A: bool(true) +d from A: bool(true) +e from A: bool(true) +f from A: bool(true) +a from global: bool(true) +b from global: bool(false) +c from global: bool(true) +d from global: bool(true) +e from global: bool(true) +f from global: bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt new file mode 100644 index 0000000000000..110d1f99eeff4 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test ReflectionProperty::isWritable() readonly +--FILE-- +a = 42; + } + + public function __clone() { + test($this); + $this->a = 43; + test($this); + } +} + +function test($instance) { + $rc = new ReflectionClass($instance); + foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from A: '; + var_dump($rp->isWritable($instance, $instance::class)); + } +} + +test(new A); +clone new A; + +?> +--EXPECT-- +a from A: bool(false) +b from A: bool(true) +a from A: bool(true) +b from A: bool(true) +a from A: bool(false) +b from A: bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt new file mode 100644 index 0000000000000..675b53c338ce1 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt @@ -0,0 +1,57 @@ +--TEST-- +Test ReflectionProperty::isWritable() visibility +--FILE-- +getProperties() as $rp) { + echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; + var_dump($rp->isWritable(null, $scope)); + } +}; + +$test('A'); +$test('B'); +$test('C'); +$test(null); + +?> +--EXPECT-- +a from A: bool(true) +b from A: bool(true) +c from A: bool(false) +d from A: bool(false) +e from A: bool(true) +f from A: bool(true) +a from B: bool(true) +b from B: bool(true) +c from B: bool(true) +d from B: bool(true) +e from B: bool(true) +f from B: bool(true) +a from C: bool(true) +b from C: bool(true) +c from C: bool(false) +d from C: bool(false) +e from C: bool(true) +f from C: bool(true) +a from global: bool(true) +b from global: bool(false) +c from global: bool(false) +d from global: bool(false) +e from global: bool(false) +f from global: bool(false) From 9f587dc764a7d7a1b43f7c9e764cebe2c5efb252 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 15 Dec 2025 16:21:19 +0100 Subject: [PATCH 3/7] Handle dynamic properties, fix order & optionality --- ext/reflection/php_reflection.c | 24 ++++++++++------- ext/reflection/php_reflection.stub.php | 4 +-- ext/reflection/php_reflection_arginfo.h | 6 ++--- ...ReflectionProperty_isReadable_dynamic.phpt | 25 +++++++++++++++++ .../ReflectionProperty_isReadable_hooks.phpt | 2 +- .../ReflectionProperty_isReadable_init.phpt | 2 +- ...lectionProperty_isReadable_visibility.phpt | 2 +- ...ReflectionProperty_isWritable_dynamic.phpt | 27 +++++++++++++++++++ .../ReflectionProperty_isWritable_hooks.phpt | 2 +- ...eflectionProperty_isWritable_readonly.phpt | 2 +- ...lectionProperty_isWritable_visibility.phpt | 2 +- 11 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index c1f67b4f5baf0..05d98b81eff6b 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6682,20 +6682,22 @@ ZEND_METHOD(ReflectionProperty, isReadable) { reflection_object *intern; property_reference *ref; + zend_string *scope_name; zend_object *obj = NULL; - zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC); - ZEND_PARSE_PARAMETERS_START(0, 2) + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR_OR_NULL(scope_name) Z_PARAM_OPTIONAL Z_PARAM_OBJ_OR_NULL(obj) - Z_PARAM_STR_OR_NULL(scope_name) ZEND_PARSE_PARAMETERS_END(); GET_REFLECTION_OBJECT_PTR(ref); zend_property_info *prop = ref->prop; if (!prop) { - _DO_THROW("May not use isReadable on dynamic properties"); - RETURN_THROWS(); + if (!obj->properties) { + RETURN_FALSE; + } + RETURN_BOOL(zend_hash_find_ptr(obj->properties, ref->unmangled_name) != NULL); } if (obj) { @@ -6751,20 +6753,22 @@ ZEND_METHOD(ReflectionProperty, isWritable) { reflection_object *intern; property_reference *ref; + zend_string *scope_name; zend_object *obj = NULL; - zend_string *scope_name = ZSTR_KNOWN(ZEND_STR_STATIC); - ZEND_PARSE_PARAMETERS_START(0, 2) + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR_OR_NULL(scope_name) Z_PARAM_OPTIONAL Z_PARAM_OBJ_OR_NULL(obj) - Z_PARAM_STR_OR_NULL(scope_name) ZEND_PARSE_PARAMETERS_END(); GET_REFLECTION_OBJECT_PTR(ref); zend_property_info *prop = ref->prop; if (!prop) { - _DO_THROW("May not use isWritable on dynamic properties"); - RETURN_THROWS(); + if (obj->ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES) { + RETURN_FALSE; + } + RETURN_TRUE; } if (obj) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 33a46925a9308..a4b97d280d8e8 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -572,9 +572,9 @@ public function getHook(PropertyHookType $type): ?ReflectionMethod {} public function isFinal(): bool {} - public function isReadable(?object $object = null, ?string $scope = 'static'): bool {} + public function isReadable(?string $scope, ?object $object = null): bool {} - public function isWritable(?object $object = null, ?string $scope = 'static'): bool {} + public function isWritable(?string $scope, ?object $object = null): bool {} } /** @not-serializable */ diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 55d8482422707..aca80acd6a8ef 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: f0018fa5d1204c853476bdd34385f3ec85285249 */ + * Stub hash: 918c9d5ef5f69a060848768d7a455577d3bb5796 */ 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) @@ -472,9 +472,9 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionProperty_isFinal arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionProperty_isReadable, 0, 0, _IS_BOOL, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionProperty_isReadable, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, scope, IS_STRING, 1) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, object, IS_OBJECT, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, scope, IS_STRING, 1, "\'static\'") ZEND_END_ARG_INFO() #define arginfo_class_ReflectionProperty_isWritable arginfo_class_ReflectionProperty_isReadable diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt new file mode 100644 index 0000000000000..bacd7f1fee2c0 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt @@ -0,0 +1,25 @@ +--TEST-- +Test ReflectionProperty::isReadable() dynamic +--FILE-- +a = 'a'; +$r = new ReflectionProperty($a, 'a'); + +var_dump($r->isReadable(null, $a)); +unset($a->a); +var_dump($r->isReadable(null, $a)); + +$a = new A; +var_dump($r->isReadable(null, $a)); + +?> +--EXPECT-- +bool(true) +bool(false) +bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt index ce00d7ac26b1a..56180155db4c1 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt @@ -16,7 +16,7 @@ $test = static function ($scope) { $rc = new ReflectionClass(A::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isReadable(null, $scope)); + var_dump($rp->isReadable($scope, null)); } }; diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt index ca55af75db536..14b0ad3b9bc47 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt @@ -49,7 +49,7 @@ $test = static function ($class) { $rc = new ReflectionClass($class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from global: '; - var_dump($rp->isReadable(new $class, NULL)); + var_dump($rp->isReadable(null, new $class)); } }; diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt index 350598c57ffa0..7db99a9baa31a 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt @@ -18,7 +18,7 @@ $test = static function ($scope) { $rc = new ReflectionClass(B::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isReadable(null, $scope)); + var_dump($rp->isReadable($scope, null)); } }; diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt new file mode 100644 index 0000000000000..cf77c41e22045 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt @@ -0,0 +1,27 @@ +--TEST-- +Test ReflectionProperty::isWritable() dynamic +--FILE-- +isWritable(null, $a)); + +$a->b = 'b'; +$r = new ReflectionProperty($a, 'b'); +var_dump($r->isWritable(null, $a)); + +$a = new A; +var_dump($r->isWritable(null, $a)); + +?> +--EXPECT-- +bool(false) +bool(true) +bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt index 29c7d11b4e29a..0cd5baf3c8792 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt @@ -16,7 +16,7 @@ $test = static function ($scope) { $rc = new ReflectionClass(A::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isWritable(null, $scope)); + var_dump($rp->isWritable($scope, null)); } }; diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt index 110d1f99eeff4..256f14895908d 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_readonly.phpt @@ -22,7 +22,7 @@ function test($instance) { $rc = new ReflectionClass($instance); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from A: '; - var_dump($rp->isWritable($instance, $instance::class)); + var_dump($rp->isWritable($instance::class, $instance)); } } diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt index 675b53c338ce1..db580c06c631b 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt @@ -20,7 +20,7 @@ $test = static function ($scope) { $rc = new ReflectionClass(B::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isWritable(null, $scope)); + var_dump($rp->isWritable($scope, null)); } }; From b00d15485e8d6e8ab48d203bb30617929d71663e Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 15 Dec 2025 17:47:42 +0100 Subject: [PATCH 4/7] Fixes for dynamic properties and magic methods --- ext/reflection/php_reflection.c | 83 +++++++++++-------- ...ReflectionProperty_isReadable_dynamic.phpt | 3 + ...ReflectionProperty_isWritable_dynamic.phpt | 5 ++ 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 05d98b81eff6b..32931bf5543ad 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6692,15 +6692,9 @@ ZEND_METHOD(ReflectionProperty, isReadable) ZEND_PARSE_PARAMETERS_END(); GET_REFLECTION_OBJECT_PTR(ref); - zend_property_info *prop = ref->prop; - if (!prop) { - if (!obj->properties) { - RETURN_FALSE; - } - RETURN_BOOL(zend_hash_find_ptr(obj->properties, ref->unmangled_name) != NULL); - } - if (obj) { + zend_property_info *prop = ref->prop; + if (prop && obj) { if (!instanceof_function(obj->ce, prop->ce)) { _DO_THROW("Given object is not an instance of the class this property was declared in"); RETURN_THROWS(); @@ -6708,13 +6702,38 @@ ZEND_METHOD(ReflectionProperty, isReadable) prop = reflection_property_get_effective_prop(ref, intern->ce, obj); } + zend_class_entry *ce = obj ? obj->ce : intern->ce; + if (!prop) { + if (obj && obj->properties && zend_hash_find_ptr(obj->properties, ref->unmangled_name)) { + RETURN_TRUE; + } +handle_magic_get: + if (ce->__get) { + if (obj && ce->__isset) { + uint32_t *guard = zend_get_property_guard(obj, ref->unmangled_name); + if (!((*guard) & ZEND_GUARD_PROPERTY_ISSET)) { + GC_ADDREF(obj); + *guard |= ZEND_GUARD_PROPERTY_ISSET; + zval member; + ZVAL_STR(&member, ref->unmangled_name); + zend_call_known_instance_method_with_1_params(ce->__isset, obj, return_value, &member); + *guard &= ~ZEND_GUARD_PROPERTY_ISSET; + OBJ_RELEASE(obj); + return; + } + } + RETURN_TRUE; + } + RETURN_FALSE; + } + zend_class_entry *scope; if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) { RETURN_THROWS(); } if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) { - RETURN_FALSE; + goto handle_magic_get; } if (prop->flags & ZEND_ACC_VIRTUAL) { @@ -6722,27 +6741,13 @@ ZEND_METHOD(ReflectionProperty, isReadable) if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) { RETURN_FALSE; } - } - - if (obj && !prop->hooks) { + } else if (obj && (!prop->hooks || !prop->hooks[ZEND_PROPERTY_HOOK_GET])) { zval *prop_val = OBJ_PROP(obj, prop->offset); - if ( Z_TYPE_P(prop_val) == IS_UNDEF) { - if (!obj->ce->__get || (Z_PROP_FLAG_P(prop_val) & IS_PROP_UNINIT)) { - RETURN_FALSE; - } - if (obj->ce->__isset) { - uint32_t *guard = zend_get_property_guard(obj, ref->unmangled_name); - if (!((*guard) & ZEND_GUARD_PROPERTY_ISSET)) { - GC_ADDREF(obj); - *guard |= ZEND_GUARD_PROPERTY_ISSET; - zval member; - ZVAL_STR(&member, ref->unmangled_name); - zend_call_known_instance_method_with_1_params(obj->ce->__isset, obj, return_value, &member); - *guard &= ~ZEND_GUARD_PROPERTY_ISSET; - OBJ_RELEASE(obj); - return; - } + if (Z_TYPE_P(prop_val) == IS_UNDEF) { + if (!(Z_PROP_FLAG_P(prop_val) & IS_PROP_UNINIT)) { + goto handle_magic_get; } + RETURN_FALSE; } } @@ -6763,15 +6768,9 @@ ZEND_METHOD(ReflectionProperty, isWritable) ZEND_PARSE_PARAMETERS_END(); GET_REFLECTION_OBJECT_PTR(ref); - zend_property_info *prop = ref->prop; - if (!prop) { - if (obj->ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES) { - RETURN_FALSE; - } - RETURN_TRUE; - } - if (obj) { + zend_property_info *prop = ref->prop; + if (prop && obj) { if (!instanceof_function(obj->ce, prop->ce)) { _DO_THROW("Given object is not an instance of the class this property was declared in"); RETURN_THROWS(); @@ -6779,11 +6778,23 @@ ZEND_METHOD(ReflectionProperty, isWritable) prop = reflection_property_get_effective_prop(ref, intern->ce, obj); } + zend_class_entry *ce = obj ? obj->ce : intern->ce; + if (!prop) { + if (!(ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) { + RETURN_TRUE; + } +handle_magic_set: + RETURN_BOOL(ce->__set); + } + zend_class_entry *scope; if (get_ce_from_scope_name(&scope, scope_name, execute_data) == FAILURE) { RETURN_THROWS(); } + if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) { + goto handle_magic_set; + } uint32_t set_visibility = prop->flags & ZEND_ACC_PPP_SET_MASK; if (!set_visibility) { set_visibility = zend_visibility_to_set_visibility(prop->flags & ZEND_ACC_PPP_MASK); diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt index bacd7f1fee2c0..a4056a60555e8 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_dynamic.phpt @@ -18,8 +18,11 @@ var_dump($r->isReadable(null, $a)); $a = new A; var_dump($r->isReadable(null, $a)); +var_dump($r->isReadable(null, null)); + ?> --EXPECT-- bool(true) bool(false) bool(false) +bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt index cf77c41e22045..e9da6675ddaef 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_dynamic.phpt @@ -12,6 +12,7 @@ $a = new A; $r = new ReflectionProperty($a, 'a'); var_dump($r->isWritable(null, $a)); +var_dump($r->isWritable(null, null)); $a->b = 'b'; $r = new ReflectionProperty($a, 'b'); @@ -20,8 +21,12 @@ var_dump($r->isWritable(null, $a)); $a = new A; var_dump($r->isWritable(null, $a)); +var_dump($r->isWritable(null, null)); + ?> --EXPECT-- bool(false) +bool(false) +bool(true) bool(true) bool(true) From cd11a77900a472e95b4e796eed23474151341dc7 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 16 Dec 2025 01:22:01 +0100 Subject: [PATCH 5/7] Test all paths --- ext/reflection/php_reflection.c | 4 +++ ...tionProperty_isReadable_invalid_scope.phpt | 20 +++++++++++ ...nProperty_isReadable_unrelated_object.phpt | 36 +++++++++++++++++++ ...lectionProperty_isReadable_visibility.phpt | 26 ++++++++++++-- ...tionProperty_isWritable_invalid_scope.phpt | 20 +++++++++++ ...nProperty_isWritable_unrelated_object.phpt | 36 +++++++++++++++++++ ...lectionProperty_isWritable_visibility.phpt | 13 ++++++- 7 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_invalid_scope.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_unrelated_object.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_invalid_scope.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_unrelated_object.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 32931bf5543ad..b7cd04edcbd1e 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6783,6 +6783,10 @@ ZEND_METHOD(ReflectionProperty, isWritable) if (!(ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) { RETURN_TRUE; } + /* This path is effectively unreachable, but theoretically possible for + * two internal classes where ZEND_ACC_NO_DYNAMIC_PROPERTIES is only + * added to the subclass, in which case a ReflectionProperty can be + * constructed on the parent class, and then tested on the subclass. */ handle_magic_set: RETURN_BOOL(ce->__set); } diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_invalid_scope.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_invalid_scope.phpt new file mode 100644 index 0000000000000..ae9f446c67719 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_invalid_scope.phpt @@ -0,0 +1,20 @@ +--TEST-- +Test ReflectionProperty::isReadable() invalid scope +--FILE-- +isReadable('B', null); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Error: Class "B" not found diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_unrelated_object.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_unrelated_object.phpt new file mode 100644 index 0000000000000..ce48958c28ce2 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_unrelated_object.phpt @@ -0,0 +1,36 @@ +--TEST-- +Test ReflectionProperty::isReadable() unrelated object +--FILE-- +isReadable(null, $obj)); + } catch (Exception $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + } +} + +test(new A); +test(new B); +test(new C); +test(new D); + +?> +--EXPECT-- +ReflectionException: Given object is not an instance of the class this property was declared in +bool(true) +bool(true) +ReflectionException: Given object is not an instance of the class this property was declared in diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt index 7db99a9baa31a..3a8a164185f75 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt @@ -9,20 +9,32 @@ class B extends A { public $a; protected $b; private $c; - public protected(set) int $d; + public protected(set) int $d = 42; } class C extends B {} +class D extends C { + public function __get($name) {} +} + +class E extends D { + private $f; + + public function __isset($name) { + return $name === 'f'; + } +} + $test = static function ($scope) { $rc = new ReflectionClass(B::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isReadable($scope, null)); + var_dump($rp->isReadable($scope, $scope && $scope !== 'A' ? new $scope : null)); } }; -foreach (['A', 'B', 'C'] as $scope) { +foreach (['A', 'B', 'C', 'D', 'E'] as $scope) { $test($scope); } $test(null); @@ -41,6 +53,14 @@ a from C: bool(true) b from C: bool(true) c from C: bool(false) d from C: bool(true) +a from D: bool(true) +b from D: bool(true) +c from D: bool(true) +d from D: bool(true) +a from E: bool(true) +b from E: bool(true) +c from E: bool(false) +d from E: bool(true) a from global: bool(true) b from global: bool(false) c from global: bool(false) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_invalid_scope.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_invalid_scope.phpt new file mode 100644 index 0000000000000..7474e521b35e4 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_invalid_scope.phpt @@ -0,0 +1,20 @@ +--TEST-- +Test ReflectionProperty::isWritable() invalid scope +--FILE-- +isWritable('B', null); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Error: Class "B" not found diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_unrelated_object.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_unrelated_object.phpt new file mode 100644 index 0000000000000..3e1e6a394c8f0 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_unrelated_object.phpt @@ -0,0 +1,36 @@ +--TEST-- +Test ReflectionProperty::isWritable() unrelated object +--FILE-- +isWritable(null, $obj)); + } catch (Exception $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + } +} + +test(new A); +test(new B); +test(new C); +test(new D); + +?> +--EXPECT-- +ReflectionException: Given object is not an instance of the class this property was declared in +bool(true) +bool(true) +ReflectionException: Given object is not an instance of the class this property was declared in diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt index db580c06c631b..396b3ae72136d 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt @@ -16,17 +16,22 @@ class B extends A { class C extends B {} +class D extends C { + public function __set($name, $value) {} +} + $test = static function ($scope) { $rc = new ReflectionClass(B::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; - var_dump($rp->isWritable($scope, null)); + var_dump($rp->isWritable($scope, $scope && $scope !== 'A' ? new $scope : null)); } }; $test('A'); $test('B'); $test('C'); +$test('D'); $test(null); ?> @@ -49,6 +54,12 @@ c from C: bool(false) d from C: bool(false) e from C: bool(true) f from C: bool(true) +a from D: bool(true) +b from D: bool(true) +c from D: bool(true) +d from D: bool(false) +e from D: bool(true) +f from D: bool(true) a from global: bool(true) b from global: bool(false) c from global: bool(false) From 318627be913fe8eb2eab76f142d975f774cdd8f4 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 16 Dec 2025 21:42:28 +0100 Subject: [PATCH 6/7] No need for these to be closures --- .../tests/ReflectionProperty_isReadable_hooks.phpt | 8 ++++---- .../tests/ReflectionProperty_isReadable_init.phpt | 10 +++++----- .../ReflectionProperty_isReadable_visibility.phpt | 8 ++++---- .../tests/ReflectionProperty_isWritable_hooks.phpt | 8 ++++---- .../ReflectionProperty_isWritable_visibility.phpt | 14 +++++++------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt index 56180155db4c1..5fc31b2163eb4 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_hooks.phpt @@ -12,16 +12,16 @@ class A { public $f { get {} set {} } } -$test = static function ($scope) { +function test($scope) { $rc = new ReflectionClass(A::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; var_dump($rp->isReadable($scope, null)); } -}; +} -$test('A'); -$test(null); +test('A'); +test(null); ?> --EXPECT-- diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt index 14b0ad3b9bc47..242f28b57a821 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_init.phpt @@ -45,17 +45,17 @@ class C { public function __get($name) {} } -$test = static function ($class) { +function test($class) { $rc = new ReflectionClass($class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from global: '; var_dump($rp->isReadable(null, new $class)); } -}; +} -$test('A'); -$test('B'); -$test('C'); +test('A'); +test('B'); +test('C'); ?> --EXPECT-- diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt index 3a8a164185f75..205ff85293ff7 100644 --- a/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt +++ b/ext/reflection/tests/ReflectionProperty_isReadable_visibility.phpt @@ -26,18 +26,18 @@ class E extends D { } } -$test = static function ($scope) { +function test($scope) { $rc = new ReflectionClass(B::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; var_dump($rp->isReadable($scope, $scope && $scope !== 'A' ? new $scope : null)); } -}; +} foreach (['A', 'B', 'C', 'D', 'E'] as $scope) { - $test($scope); + test($scope); } -$test(null); +test(null); ?> --EXPECT-- diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt index 0cd5baf3c8792..2617ec65b9e71 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_hooks.phpt @@ -12,16 +12,16 @@ class A { public $f { get {} set {} } } -$test = static function ($scope) { +function test($scope) { $rc = new ReflectionClass(A::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; var_dump($rp->isWritable($scope, null)); } -}; +} -$test('A'); -$test(null); +test('A'); +test(null); ?> --EXPECT-- diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt index 396b3ae72136d..1585f4cba531f 100644 --- a/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt +++ b/ext/reflection/tests/ReflectionProperty_isWritable_visibility.phpt @@ -20,19 +20,19 @@ class D extends C { public function __set($name, $value) {} } -$test = static function ($scope) { +function test($scope) { $rc = new ReflectionClass(B::class); foreach ($rc->getProperties() as $rp) { echo $rp->getName() . ' from ' . ($scope ?? 'global') . ': '; var_dump($rp->isWritable($scope, $scope && $scope !== 'A' ? new $scope : null)); } -}; +} -$test('A'); -$test('B'); -$test('C'); -$test('D'); -$test(null); +test('A'); +test('B'); +test('C'); +test('D'); +test(null); ?> --EXPECT-- From cab333105a0c82b75e0252e8bb267ce117b7810c Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 16 Dec 2025 22:12:49 +0100 Subject: [PATCH 7/7] Support static properties --- ext/reflection/php_reflection.c | 29 +++++++++++---- .../ReflectionProperty_isReadable_static.phpt | 36 +++++++++++++++++++ .../ReflectionProperty_isWritable_static.phpt | 36 +++++++++++++++++++ 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 ext/reflection/tests/ReflectionProperty_isReadable_static.phpt create mode 100644 ext/reflection/tests/ReflectionProperty_isWritable_static.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index b7cd04edcbd1e..16ecfb76073b8 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6695,6 +6695,10 @@ ZEND_METHOD(ReflectionProperty, isReadable) zend_property_info *prop = ref->prop; if (prop && obj) { + if (prop->flags & ZEND_ACC_STATIC) { + _DO_THROW("null is expected as object argument for static properties"); + RETURN_THROWS(); + } if (!instanceof_function(obj->ce, prop->ce)) { _DO_THROW("Given object is not an instance of the class this property was declared in"); RETURN_THROWS(); @@ -6733,7 +6737,10 @@ ZEND_METHOD(ReflectionProperty, isReadable) } if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) { - goto handle_magic_get; + if (!(prop->flags & ZEND_ACC_STATIC)) { + goto handle_magic_get; + } + RETURN_FALSE; } if (prop->flags & ZEND_ACC_VIRTUAL) { @@ -6749,6 +6756,12 @@ ZEND_METHOD(ReflectionProperty, isReadable) } RETURN_FALSE; } + } else if (prop->flags & ZEND_ACC_STATIC) { + if (ce->default_static_members_count && !CE_STATIC_MEMBERS(ce)) { + zend_class_init_statics(ce); + } + zval *prop_val = CE_STATIC_MEMBERS(ce) + prop->offset; + RETURN_BOOL(!Z_ISUNDEF_P(prop_val)); } RETURN_TRUE; @@ -6771,6 +6784,10 @@ ZEND_METHOD(ReflectionProperty, isWritable) zend_property_info *prop = ref->prop; if (prop && obj) { + if (prop->flags & ZEND_ACC_STATIC) { + _DO_THROW("null is expected as object argument for static properties"); + RETURN_THROWS(); + } if (!instanceof_function(obj->ce, prop->ce)) { _DO_THROW("Given object is not an instance of the class this property was declared in"); RETURN_THROWS(); @@ -6797,7 +6814,10 @@ ZEND_METHOD(ReflectionProperty, isWritable) } if (!check_visibility(prop->flags & ZEND_ACC_PPP_MASK, prop->ce, scope)) { - goto handle_magic_set; + if (!(prop->flags & ZEND_ACC_STATIC)) { + goto handle_magic_set; + } + RETURN_FALSE; } uint32_t set_visibility = prop->flags & ZEND_ACC_PPP_SET_MASK; if (!set_visibility) { @@ -6812,10 +6832,7 @@ ZEND_METHOD(ReflectionProperty, isWritable) if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) { RETURN_FALSE; } - } - - if (obj && (prop->flags & ZEND_ACC_READONLY)) { - ZEND_ASSERT(prop->offset != ZEND_VIRTUAL_PROPERTY_OFFSET); + } else if (obj && (prop->flags & ZEND_ACC_READONLY)) { zval *prop_val = OBJ_PROP(obj, prop->offset); if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) { RETURN_FALSE; diff --git a/ext/reflection/tests/ReflectionProperty_isReadable_static.phpt b/ext/reflection/tests/ReflectionProperty_isReadable_static.phpt new file mode 100644 index 0000000000000..e927f428a1f82 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isReadable_static.phpt @@ -0,0 +1,36 @@ +--TEST-- +Test ReflectionProperty::isReadable() static +--FILE-- +isReadable(null, new A); +} catch (Exception $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +$rc = new ReflectionClass('A'); +foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from global: '; + var_dump($rp->isReadable(null)); +} + +?> +--EXPECT-- +ReflectionException: null is expected as object argument for static properties +a from global: bool(true) +b from global: bool(false) +c from global: bool(true) +d from global: bool(false) +e from global: bool(false) +f from global: bool(true) diff --git a/ext/reflection/tests/ReflectionProperty_isWritable_static.phpt b/ext/reflection/tests/ReflectionProperty_isWritable_static.phpt new file mode 100644 index 0000000000000..008d585c31530 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_isWritable_static.phpt @@ -0,0 +1,36 @@ +--TEST-- +Test ReflectionProperty::isWritable() static +--FILE-- +isWritable(null, new A); +} catch (Exception $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +$rc = new ReflectionClass('A'); +foreach ($rc->getProperties() as $rp) { + echo $rp->getName() . ' from global: '; + var_dump($rp->isWritable(null)); +} + +?> +--EXPECT-- +ReflectionException: null is expected as object argument for static properties +a from global: bool(true) +b from global: bool(true) +c from global: bool(true) +d from global: bool(false) +e from global: bool(false) +f from global: bool(false)