Commit 020d4741745 for php
commit 020d474174530e5d98dbc33d09914038fb5e6e00
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date: Fri Nov 6 21:08:04 2026 +0500
Fix GH-25238: ReflectionProperty::isInitialized() skips lazy init once cached (#25239)
The inline cache fast path in isInitialized() returned false as soon as the cached slot was UNDEF. All properties of an uninitialized lazy object are UNDEF, and so are the slots of a lazy proxy, so once the cache was warm, later lazy objects of the same class were reported as uninitialized and never got initialized. The fast path now falls back to has_property() only when the slot is UNDEF and the object is lazy. Non-lazy objects still get the cached answer.
diff --git a/NEWS b/NEWS
index 29bb88d40ec..287f2b1d0a4 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,8 @@ PHP NEWS
of class names in implements, catch, and trait use lists. (Jorg Sowa)
. Fixed AST export dropping the braces of dynamic class constant fetches.
(Jorg Sowa)
+ . Fixed bug GH-25238 (ReflectionProperty::isInitialized() skips lazy init once
+ cached). (lazerg)
- Curl:
. Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)
diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index ea78bf601d4..b799b034764 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -6488,7 +6488,9 @@ ZEND_METHOD(ReflectionProperty, isInitialized)
if (EXPECTED(IS_VALID_PROPERTY_OFFSET(prop_offset))) {
zval *value = OBJ_PROP(Z_OBJ_P(object), prop_offset);
- RETURN_BOOL(!Z_ISUNDEF_P(value));
+ if (EXPECTED(!Z_ISUNDEF_P(value) || !zend_object_is_lazy(Z_OBJ_P(object)))) {
+ RETURN_BOOL(!Z_ISUNDEF_P(value));
+ }
}
}
diff --git a/ext/reflection/tests/gh25238.phpt b/ext/reflection/tests/gh25238.phpt
new file mode 100644
index 00000000000..24c4b754cab
--- /dev/null
+++ b/ext/reflection/tests/gh25238.phpt
@@ -0,0 +1,74 @@
+--TEST--
+GH-25238 (ReflectionProperty::isInitialized() does not initialize lazy objects once its cache slot is warm)
+--FILE--
+<?php
+
+class Line {
+ public string $label = '';
+}
+
+class NoDefault {
+ public string $label;
+}
+
+$class = new ReflectionClass(Line::class);
+$property = new ReflectionProperty(Line::class, 'label');
+
+foreach (['first', 'second', 'third'] as $label) {
+ $line = $class->newLazyGhost(function (Line $line) use ($label) {
+ $line->label = $label;
+ });
+ var_dump($property->isInitialized($line));
+ var_dump($class->isUninitializedLazyObject($line));
+ var_dump($line->label);
+}
+
+foreach (['first', 'second'] as $label) {
+ $line = $class->newLazyProxy(function () use ($label) {
+ $line = new Line();
+ $line->label = $label;
+ return $line;
+ });
+ var_dump($property->isInitialized($line));
+ var_dump($class->isUninitializedLazyObject($line));
+ var_dump($property->isInitialized($line));
+ var_dump($line->label);
+}
+
+$line = new Line();
+unset($line->label);
+var_dump($property->isInitialized($line));
+
+$class = new ReflectionClass(NoDefault::class);
+$property = new ReflectionProperty(NoDefault::class, 'label');
+
+for ($i = 0; $i < 2; $i++) {
+ $object = $class->newLazyGhost(function (NoDefault $object) {});
+ var_dump($property->isInitialized($object));
+ var_dump($class->isUninitializedLazyObject($object));
+}
+
+?>
+--EXPECT--
+bool(true)
+bool(false)
+string(5) "first"
+bool(true)
+bool(false)
+string(6) "second"
+bool(true)
+bool(false)
+string(5) "third"
+bool(true)
+bool(false)
+bool(true)
+string(5) "first"
+bool(true)
+bool(false)
+bool(true)
+string(6) "second"
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+bool(false)