From 7f3734620e33915eee40c45d151d2e66d3a56e08 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 6 Nov 2026 16:39:02 +0500 Subject: [PATCH 1/4] Fix GH-25238: ReflectionProperty::isInitialized() skips lazy init once cached --- ext/reflection/php_reflection.c | 4 +++- ext/reflection/tests/gh25238.phpt | 37 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 ext/reflection/tests/gh25238.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index ea78bf601d48..a3c51a6fd2c3 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))) { + RETURN_TRUE; + } } } diff --git a/ext/reflection/tests/gh25238.phpt b/ext/reflection/tests/gh25238.phpt new file mode 100644 index 000000000000..298f28ef434d --- /dev/null +++ b/ext/reflection/tests/gh25238.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-25238 (ReflectionProperty::isInitialized() does not initialize lazy objects once its cache slot is warm) +--FILE-- +newLazyGhost(function (Line $line) use ($label) { + $line->label = $label; + }); + var_dump($property->isInitialized($line)); + var_dump($class->isUninitializedLazyObject($line)); + var_dump($line->label); +} + +$line = new Line(); +unset($line->label); +var_dump($property->isInitialized($line)); + +?> +--EXPECT-- +bool(true) +bool(false) +string(5) "first" +bool(true) +bool(false) +string(6) "second" +bool(true) +bool(false) +string(5) "third" +bool(false) From ab0be4438c3c24e2e5d066c811b743ed84c753d9 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 6 Nov 2026 17:32:32 +0500 Subject: [PATCH 2/4] Keep isInitialized() fast path for non-lazy objects and test lazy proxies --- ext/reflection/php_reflection.c | 4 ++-- ext/reflection/tests/gh25238.phpt | 39 ++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index a3c51a6fd2c3..b799b0347645 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6488,8 +6488,8 @@ ZEND_METHOD(ReflectionProperty, isInitialized) if (EXPECTED(IS_VALID_PROPERTY_OFFSET(prop_offset))) { zval *value = OBJ_PROP(Z_OBJ_P(object), prop_offset); - if (EXPECTED(!Z_ISUNDEF_P(value))) { - RETURN_TRUE; + 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 index 298f28ef434d..e3a6e5c05cd6 100644 --- a/ext/reflection/tests/gh25238.phpt +++ b/ext/reflection/tests/gh25238.phpt @@ -7,6 +7,10 @@ class Line { public string $label = ''; } +class Empty_ { + public string $label; +} + $class = new ReflectionClass(Line::class); $property = new ReflectionProperty(Line::class, 'label'); @@ -19,9 +23,30 @@ foreach (['first', 'second', 'third'] as $label) { 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); +} + +$class = new ReflectionClass(Empty_::class); +$property = new ReflectionProperty(Empty_::class, 'label'); + +foreach ([1, 2] as $i) { + $empty = $class->newLazyGhost(function (Empty_ $empty) {}); + var_dump($property->isInitialized($empty)); + var_dump($class->isUninitializedLazyObject($empty)); +} + $line = new Line(); unset($line->label); -var_dump($property->isInitialized($line)); +var_dump((new ReflectionProperty(Line::class, 'label'))->isInitialized($line)); ?> --EXPECT-- @@ -34,4 +59,16 @@ 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) From 20aba073e42acd1a6f41dda55b618a9cb376a52c Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 6 Nov 2026 17:36:49 +0500 Subject: [PATCH 3/4] Cover warm cache fast path for non-lazy objects in GH-25238 test --- ext/reflection/tests/gh25238.phpt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ext/reflection/tests/gh25238.phpt b/ext/reflection/tests/gh25238.phpt index e3a6e5c05cd6..24c4b754cabc 100644 --- a/ext/reflection/tests/gh25238.phpt +++ b/ext/reflection/tests/gh25238.phpt @@ -7,7 +7,7 @@ class Line { public string $label = ''; } -class Empty_ { +class NoDefault { public string $label; } @@ -35,18 +35,18 @@ foreach (['first', 'second'] as $label) { var_dump($line->label); } -$class = new ReflectionClass(Empty_::class); -$property = new ReflectionProperty(Empty_::class, 'label'); - -foreach ([1, 2] as $i) { - $empty = $class->newLazyGhost(function (Empty_ $empty) {}); - var_dump($property->isInitialized($empty)); - var_dump($class->isUninitializedLazyObject($empty)); -} - $line = new Line(); unset($line->label); -var_dump((new ReflectionProperty(Line::class, 'label'))->isInitialized($line)); +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-- From bb34664a58a593467658c8eb9b437ef717dbacbe Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 6 Nov 2026 17:06:57 +0100 Subject: [PATCH 4/4] [ci skip] NEWS --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 29bb88d40ec0..287f2b1d0a4a 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)