[php-src] PHP-8.5: Report dynamic property shadowing a private parent in Reflection

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-06-25T18:50:43-04:00

Commit: https://github.com/php/php-src/commit/86d78cc24f174aa0b5545df7c1336be00da1921e
Raw diff: https://github.com/php/php-src/commit/86d78cc24f174aa0b5545df7c1336be00da1921e.diff

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

Changed paths:
  A  ext/reflection/tests/gh22441.phpt
  M  NEWS
  M  ext/reflection/php_reflection.c


Diff:

diff --git a/NEWS b/NEWS
index 39f2359152f0..0f39334377e0 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 eba5600c16a2..e747a642778c 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 000000000000..9ca3cb0eee74
--- /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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.