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