Commit ae42ecf1f34 for php.net
commit ae42ecf1f349034b77b89da1b607098de15e66bf
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jul 31 07:50:45 2026 -0400
Resolve the effective property in ReflectionProperty lazy APIs
skipLazyInitialization() and isLazy() indexed the object with the
property info of the reflected scope. When a child adds hooks to a plain
parent property the child gets its own storage slot, so a parent-scoped
ReflectionProperty read and wrote the dead parent slot: isLazy() reported
false and skipLazyInitialization() was a silent no-op. Resolve the
effective property first, as getRawValue(), setRawValue() and
setRawValueWithoutLazyInitialization() already do.
Closes GH-22969
diff --git a/Zend/tests/lazy_objects/isLazy_overridden_hooked_prop.phpt b/Zend/tests/lazy_objects/isLazy_overridden_hooked_prop.phpt
new file mode 100644
index 00000000000..615b9c2bb5c
--- /dev/null
+++ b/Zend/tests/lazy_objects/isLazy_overridden_hooked_prop.phpt
@@ -0,0 +1,48 @@
+--TEST--
+Lazy Objects: ReflectionProperty::isLazy() with a hooked override
+--FILE--
+<?php
+
+class B {
+ public $plain = 1;
+ public $virtual { get => 1; }
+ private $priv = 1;
+ public static $stat;
+}
+
+class C extends B {
+ public $plain { get => $this->plain; set => $this->plain = $value; }
+ public $priv = 2;
+}
+
+$reflector = new ReflectionClass(C::class);
+
+foreach (['Ghost', 'Proxy'] as $kind) {
+ printf("# %s:\n", $kind);
+
+ $obj = $kind === 'Ghost'
+ ? $reflector->newLazyGhost(function ($obj) { })
+ : $reflector->newLazyProxy(function ($obj) { return new C(); });
+
+ foreach (['plain', 'virtual', 'stat'] as $name) {
+ printf("%s: B scope %d, C scope %d\n", $name,
+ (new ReflectionProperty(B::class, $name))->isLazy($obj),
+ (new ReflectionProperty(C::class, $name))->isLazy($obj));
+ }
+
+ printf("priv: B scope %d\n",
+ (new ReflectionProperty(B::class, 'priv'))->isLazy($obj));
+}
+
+?>
+--EXPECT--
+# Ghost:
+plain: B scope 1, C scope 1
+virtual: B scope 0, C scope 0
+stat: B scope 0, C scope 0
+priv: B scope 1
+# Proxy:
+plain: B scope 1, C scope 1
+virtual: B scope 0, C scope 0
+stat: B scope 0, C scope 0
+priv: B scope 1
diff --git a/Zend/tests/lazy_objects/skipLazyInitialization_overridden_hooked_prop.phpt b/Zend/tests/lazy_objects/skipLazyInitialization_overridden_hooked_prop.phpt
new file mode 100644
index 00000000000..818bf17915d
--- /dev/null
+++ b/Zend/tests/lazy_objects/skipLazyInitialization_overridden_hooked_prop.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Lazy Objects: ReflectionProperty::skipLazyInitialization() with a hooked override
+--FILE--
+<?php
+
+class B {
+ public $b = 1;
+}
+
+class C extends B {
+ public $b { get => $this->b; set => $this->b = $value; }
+}
+
+$reflector = new ReflectionClass(C::class);
+
+foreach (['Ghost', 'Proxy'] as $kind) {
+ printf("# %s:\n", $kind);
+
+ $obj = $kind === 'Ghost'
+ ? $reflector->newLazyGhost(function ($obj) { $obj->b = 9; })
+ : $reflector->newLazyProxy(function ($obj) { $c = new C(); $c->b = 9; return $c; });
+
+ (new ReflectionProperty(B::class, 'b'))->skipLazyInitialization($obj);
+
+ printf("is lazy: %d\n", $reflector->isUninitializedLazyObject($obj));
+ var_dump($obj->b);
+}
+
+?>
+--EXPECT--
+# Ghost:
+is lazy: 0
+NULL
+# Proxy:
+is lazy: 0
+NULL
diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index f153cf83e14..34ea93305bc 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -6365,19 +6365,22 @@ ZEND_METHOD(ReflectionProperty, skipLazyInitialization)
Z_PARAM_OBJ_OF_CLASS(object, intern->ce)
} ZEND_PARSE_PARAMETERS_END();
- if (reflection_property_check_lazy_compatible(ref->prop,
- ref->unmangled_name, intern, object,
- "skipLazyInitialization") == FAILURE) {
- RETURN_THROWS();
- }
-
while (zend_object_is_lazy_proxy(object)
&& zend_lazy_object_initialized(object)) {
object = zend_lazy_object_get_instance(object);
}
- zval *src = &object->ce->default_properties_table[OBJ_PROP_TO_NUM(ref->prop->offset)];
- zval *dst = OBJ_PROP(object, ref->prop->offset);
+ zend_property_info *prop = reflection_property_get_effective_prop(ref,
+ intern->ce, object);
+
+ if (reflection_property_check_lazy_compatible(prop,
+ ref->unmangled_name, intern, object,
+ "skipLazyInitialization") == FAILURE) {
+ RETURN_THROWS();
+ }
+
+ zval *src = &object->ce->default_properties_table[OBJ_PROP_TO_NUM(prop->offset)];
+ zval *dst = OBJ_PROP(object, prop->offset);
if (!(Z_PROP_FLAG_P(dst) & IS_PROP_LAZY)) {
/* skipLazyInitialization has no effect on non-lazy properties */
@@ -6409,16 +6412,19 @@ ZEND_METHOD(ReflectionProperty, isLazy)
Z_PARAM_OBJ_OF_CLASS(object, intern->ce)
} ZEND_PARSE_PARAMETERS_END();
- if (!ref->prop || ref->prop->flags & (ZEND_ACC_STATIC | ZEND_ACC_VIRTUAL)) {
- RETURN_FALSE;
- }
-
while (zend_object_is_lazy_proxy(object)
&& zend_lazy_object_initialized(object)) {
object = zend_lazy_object_get_instance(object);
}
- RETURN_BOOL(Z_PROP_FLAG_P(OBJ_PROP(object, ref->prop->offset)) & IS_PROP_LAZY);
+ const zend_property_info *prop = reflection_property_get_effective_prop(ref,
+ intern->ce, object);
+
+ if (!prop || prop->flags & (ZEND_ACC_STATIC | ZEND_ACC_VIRTUAL)) {
+ RETURN_FALSE;
+ }
+
+ RETURN_BOOL(Z_PROP_FLAG_P(OBJ_PROP(object, prop->offset)) & IS_PROP_LAZY);
}
/* {{{ Returns true if property was initialized */