Commit 86d78cc24f1 for php.net
commit 86d78cc24f174aa0b5545df7c1336be00da1921e
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Jun 24 20:01:08 2026 -0400
Report dynamic property shadowing a private parent in Reflection
ReflectionClass::hasProperty() and getProperty() look the name up in the
class's properties_info table and, on a match, return before checking the
object's dynamic properties. A private property declared on a parent class
lives in that table but isn't visible to the child, so a dynamic property of
the same name went unreported. Fold the accessibility check into the lookup so
an inaccessible private-parent match falls through to the existing
dynamic-property check, matching property_exists().
Fixes GH-22441
Closes GH-22451
diff --git a/NEWS b/NEWS
index 39f2359152f..0f39334377e 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,8 @@ PHP NEWS
- Reflection:
. Fixed bug GH-22324 (Ignore leading namespace separator in
ReflectionParameter::__construct()). (jorgsowa)
+ . Fixed bug GH-22441 (ReflectionClass::hasProperty() and getProperty() ignore
+ dynamic properties shadowing a private parent property). (iliaal)
- SPL:
. Fix class_parents for classes with leading slash in non-autoload mode.
diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index eba5600c16a..e747a642778 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -4699,19 +4699,17 @@ ZEND_METHOD(ReflectionClass, hasProperty)
}
GET_REFLECTION_OBJECT_PTR(ce);
- if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL) {
- if ((property_info->flags & ZEND_ACC_PRIVATE) && property_info->ce != ce) {
- RETURN_FALSE;
- }
+ if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL
+ && (!(property_info->flags & ZEND_ACC_PRIVATE)
+ || property_info->ce == ce)) {
RETURN_TRUE;
- } else {
- if (Z_TYPE(intern->obj) != IS_UNDEF) {
- if (Z_OBJ_HANDLER(intern->obj, has_property)(Z_OBJ(intern->obj), name, 2, NULL)) {
- RETURN_TRUE;
- }
+ }
+ if (Z_TYPE(intern->obj) != IS_UNDEF) {
+ if (Z_OBJ_HANDLER(intern->obj, has_property)(Z_OBJ(intern->obj), name, 2, NULL)) {
+ RETURN_TRUE;
}
- RETURN_FALSE;
}
+ RETURN_FALSE;
}
/* }}} */
@@ -4730,12 +4728,13 @@ ZEND_METHOD(ReflectionClass, getProperty)
}
GET_REFLECTION_OBJECT_PTR(ce);
- if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL) {
- if (!(property_info->flags & ZEND_ACC_PRIVATE) || property_info->ce == ce) {
- reflection_property_factory(ce, name, property_info, return_value);
- return;
- }
- } else if (Z_TYPE(intern->obj) != IS_UNDEF) {
+ if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL
+ && (!(property_info->flags & ZEND_ACC_PRIVATE)
+ || property_info->ce == ce)) {
+ reflection_property_factory(ce, name, property_info, return_value);
+ return;
+ }
+ if (Z_TYPE(intern->obj) != IS_UNDEF) {
/* Check for dynamic properties */
if (zend_hash_exists(Z_OBJ_HT(intern->obj)->get_properties(Z_OBJ(intern->obj)), name)) {
reflection_property_factory(ce, name, NULL, return_value);
diff --git a/ext/reflection/tests/gh22441.phpt b/ext/reflection/tests/gh22441.phpt
new file mode 100644
index 00000000000..9ca3cb0eee7
--- /dev/null
+++ b/ext/reflection/tests/gh22441.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-22441 (ReflectionClass::hasProperty()/getProperty() ignore dynamic properties shadowing a private parent property)
+--FILE--
+<?php
+
+class Base {
+ private mixed $shadow;
+ private mixed $onlyBase;
+}
+
+#[AllowDynamicProperties]
+class Child extends Base {}
+
+$o = new Child();
+$o->shadow = true;
+$o->noShadow = true;
+
+$r = new ReflectionObject($o);
+
+echo "hasProperty:\n";
+echo "shadow (dynamic over private parent): "; var_dump($r->hasProperty('shadow'));
+echo "noShadow (plain dynamic): "; var_dump($r->hasProperty('noShadow'));
+echo "onlyBase (private parent, no dynamic): "; var_dump($r->hasProperty('onlyBase'));
+
+echo "\ngetProperty:\n";
+foreach (['shadow', 'noShadow', 'onlyBase'] as $name) {
+ try {
+ $p = $r->getProperty($name);
+ printf("%s: %s::\$%s\n", $name, $p->getDeclaringClass()->getName(), $p->getName());
+ } catch (ReflectionException $e) {
+ printf("%s: %s\n", $name, $e->getMessage());
+ }
+}
+?>
+--EXPECT--
+hasProperty:
+shadow (dynamic over private parent): bool(true)
+noShadow (plain dynamic): bool(true)
+onlyBase (private parent, no dynamic): bool(false)
+
+getProperty:
+shadow: Child::$shadow
+noShadow: Child::$noShadow
+onlyBase: Property Child::$onlyBase does not exist