From abf4bb2231d942240b4709abf05052c1bbb6442e Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Thu, 7 Aug 2025 19:53:59 +1000 Subject: [PATCH 01/11] Add a `ReflectionClass::newInstanceFromData()` method that behaves the same as `PDOStatement::fetchObject()` --- ext/reflection/php_reflection.c | 56 ++++++++++++ ext/reflection/php_reflection.stub.php | 3 + ext/reflection/php_reflection_arginfo.h | 9 +- ...flectionClass_newInstanceFromData_001.phpt | 90 +++++++++++++++++++ .../tests/ReflectionClass_toString_001.phpt | 11 ++- 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index b5ca21d500a82..54c1296000feb 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5132,6 +5132,62 @@ ZEND_METHOD(ReflectionClass, newInstanceArgs) } /* }}} */ +/* {{{ Returns an instance of this class whose properties are filled with the given data before the constructor is called */ +ZEND_METHOD(ReflectionClass, newInstanceFromData) +{ + reflection_object *intern; + zend_class_entry *ce; + int argc = 0; + HashTable *data, *args = NULL; + zend_function *constructor; + zend_string *key; + zval *val; + + GET_REFLECTION_OBJECT_PTR(ce); + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(data) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT(args) + ZEND_PARSE_PARAMETERS_END(); + + if (args) { + argc = zend_hash_num_elements(args); + } + + if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(data, key, val) { + zend_update_property_ex(ce, Z_OBJ_P(return_value), key, val); + } ZEND_HASH_FOREACH_END(); + + const zend_class_entry *old_scope = EG(fake_scope); + EG(fake_scope) = ce; + constructor = Z_OBJ_HT_P(return_value)->get_constructor(Z_OBJ_P(return_value)); + EG(fake_scope) = old_scope; + + /* Run the constructor if there is one */ + if (constructor) { + if (!(constructor->common.fn_flags & ZEND_ACC_PUBLIC)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, "Access to non-public constructor of class %s", ZSTR_VAL(ce->name)); + zval_ptr_dtor(return_value); + RETURN_NULL(); + } + + zend_call_known_function( + constructor, Z_OBJ_P(return_value), Z_OBJCE_P(return_value), NULL, 0, NULL, args); + + if (EG(exception)) { + zend_object_store_ctor_failed(Z_OBJ_P(return_value)); + } + } else if (argc) { + zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s does not have a constructor, so you cannot pass any constructor arguments", ZSTR_VAL(ce->name)); + } +} +/* }}} */ + void reflection_class_new_lazy(INTERNAL_FUNCTION_PARAMETERS, int strategy, bool is_reset) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 7af884953bef3..146c98ac0cd62 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -371,6 +371,9 @@ public function newInstanceWithoutConstructor(): object {} /** @tentative-return-type */ public function newInstanceArgs(array $args = []): ?object {} + /** @tentative-return-type */ + public function newInstanceFromData(array $data, array $args = []): ?object {} + public function newLazyGhost(callable $initializer, int $options = 0): object {} public function newLazyProxy(callable $factory, int $options = 0): object {} diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 907ada13efab1..29ace302dc88a 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 0f6ecac0c6c4fb4af140a1be95f6a50c7532dae9 */ + * Stub hash: d7aa264ff59eaf10b71955bc55eee2b1e03081e3 */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0) @@ -289,6 +289,11 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, args, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_newInstanceFromData, 0, 1, IS_OBJECT, 1) + ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, args, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_newLazyGhost, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, initializer, IS_CALLABLE, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "0") @@ -829,6 +834,7 @@ ZEND_METHOD(ReflectionClass, isInstance); ZEND_METHOD(ReflectionClass, newInstance); ZEND_METHOD(ReflectionClass, newInstanceWithoutConstructor); ZEND_METHOD(ReflectionClass, newInstanceArgs); +ZEND_METHOD(ReflectionClass, newInstanceFromData); ZEND_METHOD(ReflectionClass, newLazyGhost); ZEND_METHOD(ReflectionClass, newLazyProxy); ZEND_METHOD(ReflectionClass, resetAsLazyGhost); @@ -1122,6 +1128,7 @@ static const zend_function_entry class_ReflectionClass_methods[] = { ZEND_ME(ReflectionClass, newInstance, arginfo_class_ReflectionClass_newInstance, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, newInstanceWithoutConstructor, arginfo_class_ReflectionClass_newInstanceWithoutConstructor, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, newInstanceArgs, arginfo_class_ReflectionClass_newInstanceArgs, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, newInstanceFromData, arginfo_class_ReflectionClass_newInstanceFromData, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, newLazyGhost, arginfo_class_ReflectionClass_newLazyGhost, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, newLazyProxy, arginfo_class_ReflectionClass_newLazyProxy, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, resetAsLazyGhost, arginfo_class_ReflectionClass_resetAsLazyGhost, ZEND_ACC_PUBLIC) diff --git a/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt new file mode 100644 index 0000000000000..4ebe11226080e --- /dev/null +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt @@ -0,0 +1,90 @@ +--TEST-- +ReflectionClass::newInstanceFromData +--FILE-- +newInstanceFromData(['a' => 'bad', 'b' => 123], ['foo', 1337]); +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + +var_dump($rcA->newInstanceFromData(['a' => 123, 'b' => 'good'], ['foo', 1337])); + +var_dump($rcB->newInstanceFromData(['a' => 123, 'b' => 'good'])); + +var_dump($rcC->newInstanceFromData(['a' => 123, 'b' => 'good'])); + +var_dump($rcC->newInstanceFromData([])); + +var_dump($rcD->newInstanceFromData(['a' => 123, 'b' => 'good'])); + +?> +--EXPECTF-- +Exception: Cannot assign string to property A::$a of type int +In constructor of class A +object(A)#5 (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} +object(B)#5 (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} + +Deprecated: Creation of dynamic property C::$a is deprecated in %s on line %d + +Deprecated: Creation of dynamic property C::$b is deprecated in %s on line %d +object(C)#5 (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} +object(C)#5 (0) { +} +object(D)#5 (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index fd5d83e917419..0008d6c6a48aa 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -30,7 +30,7 @@ Class [ class ReflectionClass implements Stringable, Refle Property [ public string $name ] } - - Methods [64] { + - Methods [65] { Method [ private method __clone ] { - Parameters [0] { @@ -332,6 +332,15 @@ Class [ class ReflectionClass implements Stringable, Refle - Tentative return [ ?object ] } + Method [ public method newInstanceFromData ] { + + - Parameters [2] { + Parameter #0 [ array $data ] + Parameter #1 [ array $args = [] ] + } + - Tentative return [ ?object ] + } + Method [ public method newLazyGhost ] { - Parameters [2] { From 7158ac977100667f53b5a4176394a9a8a42346fb Mon Sep 17 00:00:00 2001 From: Matt D Date: Fri, 8 Aug 2025 09:58:01 +1000 Subject: [PATCH 02/11] Update ext/reflection/php_reflection.stub.php Co-authored-by: Gina Peter Banyard --- ext/reflection/php_reflection.stub.php | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 146c98ac0cd62..5c7f06a91d381 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -371,7 +371,6 @@ public function newInstanceWithoutConstructor(): object {} /** @tentative-return-type */ public function newInstanceArgs(array $args = []): ?object {} - /** @tentative-return-type */ public function newInstanceFromData(array $data, array $args = []): ?object {} public function newLazyGhost(callable $initializer, int $options = 0): object {} From 917e223568feceece3e09a2300cc1a26e3369e63 Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Fri, 8 Aug 2025 18:33:43 +1000 Subject: [PATCH 03/11] fix build now that we don't have a tentative return type --- ext/reflection/php_reflection_arginfo.h | 2 +- ext/reflection/tests/ReflectionClass_toString_001.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 29ace302dc88a..e31881bfcc672 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -289,7 +289,7 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, args, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_newInstanceFromData, 0, 1, IS_OBJECT, 1) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_newInstanceFromData, 0, 1, IS_OBJECT, 1) ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, args, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index 0008d6c6a48aa..8d5beb16fc7ee 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -338,7 +338,7 @@ Class [ class ReflectionClass implements Stringable, Refle Parameter #0 [ array $data ] Parameter #1 [ array $args = [] ] } - - Tentative return [ ?object ] + - Return [ ?object ] } Method [ public method newLazyGhost ] { From c736c6df0435302e2cf078735b6eca2d2013eecb Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Fri, 8 Aug 2025 19:07:13 +1000 Subject: [PATCH 04/11] prevent users from instantiating internal classes from data because it sets arbitrary properties before calling the constructor which can have weird consequences --- ext/reflection/php_reflection.c | 5 ++++ ...flectionClass_newInstanceFromData_002.phpt | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 54c1296000feb..64c343bc3b4c3 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5145,6 +5145,11 @@ ZEND_METHOD(ReflectionClass, newInstanceFromData) GET_REFLECTION_OBJECT_PTR(ce); + if (ce->type == ZEND_INTERNAL_CLASS) { + zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s is an internal class that cannot be instantiated from data", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_ARRAY_HT(data) Z_PARAM_OPTIONAL diff --git a/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt new file mode 100644 index 0000000000000..0852bbbd55f2b --- /dev/null +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt @@ -0,0 +1,30 @@ +--TEST-- +ReflectionClass::newInstanceFromData - internal class +--FILE-- +newInstanceFromData([], ['now', new DateTimeZone('UTC')]); +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + +try +{ + $rcPDOStatement->newInstanceFromData(['a' => 123]); +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + +?> +--EXPECTF-- +Exception: Class DateTime is an internal class that cannot be instantiated from data +Exception: Class PDOStatement is an internal class that cannot be instantiated from data From 7fa88f878b6f847884d9f6205ecff4af0fe39bec Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Fri, 8 Aug 2025 19:59:13 +1000 Subject: [PATCH 05/11] there we go - now i know how stub files work --- ext/reflection/php_reflection_arginfo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index e31881bfcc672..6c67c0beda43a 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: d7aa264ff59eaf10b71955bc55eee2b1e03081e3 */ + * Stub hash: cebf3006f3e20da18dd9d886d883a93bda74e18d */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0) From d10df058b4525e4b205db14f3f91cb51cf4cdf63 Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Thu, 14 Aug 2025 21:30:35 +1000 Subject: [PATCH 06/11] don't return null anymore --- ext/reflection/php_reflection.c | 2 +- ext/reflection/php_reflection.stub.php | 2 +- ext/reflection/php_reflection_arginfo.h | 4 ++-- ext/reflection/tests/ReflectionClass_toString_001.phpt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 64c343bc3b4c3..79400b618519d 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5178,7 +5178,7 @@ ZEND_METHOD(ReflectionClass, newInstanceFromData) if (!(constructor->common.fn_flags & ZEND_ACC_PUBLIC)) { zend_throw_exception_ex(reflection_exception_ptr, 0, "Access to non-public constructor of class %s", ZSTR_VAL(ce->name)); zval_ptr_dtor(return_value); - RETURN_NULL(); + RETURN_THROWS(); } zend_call_known_function( diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 5c7f06a91d381..06a1a6054d448 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -371,7 +371,7 @@ public function newInstanceWithoutConstructor(): object {} /** @tentative-return-type */ public function newInstanceArgs(array $args = []): ?object {} - public function newInstanceFromData(array $data, array $args = []): ?object {} + public function newInstanceFromData(array $data, array $args = []): object {} public function newLazyGhost(callable $initializer, int $options = 0): object {} diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 6c67c0beda43a..1f2227a62e3e3 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: cebf3006f3e20da18dd9d886d883a93bda74e18d */ + * Stub hash: f1ef7ac004010afadaa995fc57052de576a66f5d */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0) @@ -289,7 +289,7 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, args, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_newInstanceFromData, 0, 1, IS_OBJECT, 1) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_newInstanceFromData, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, args, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index 8d5beb16fc7ee..40bc1466ca483 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -338,7 +338,7 @@ Class [ class ReflectionClass implements Stringable, Refle Parameter #0 [ array $data ] Parameter #1 [ array $args = [] ] } - - Return [ ?object ] + - Return [ object ] } Method [ public method newLazyGhost ] { From a3d6450401759a59bbf072bb2d49ece0e3a48766 Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Thu, 14 Aug 2025 21:42:37 +1000 Subject: [PATCH 07/11] make sure we lift exceptions up the call stack --- ext/reflection/php_reflection.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 79400b618519d..39977285e5502 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5161,7 +5161,7 @@ ZEND_METHOD(ReflectionClass, newInstanceFromData) } if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { - return; + RETURN_THROWS(); } ZEND_HASH_FOREACH_STR_KEY_VAL(data, key, val) { @@ -5186,9 +5186,11 @@ ZEND_METHOD(ReflectionClass, newInstanceFromData) if (EG(exception)) { zend_object_store_ctor_failed(Z_OBJ_P(return_value)); + RETURN_THROWS(); } } else if (argc) { zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s does not have a constructor, so you cannot pass any constructor arguments", ZSTR_VAL(ce->name)); + RETURN_THROWS(); } } /* }}} */ From b2518be199640b96ecf2003492a8fe2f38c3916a Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Thu, 14 Aug 2025 22:22:30 +1000 Subject: [PATCH 08/11] add tests for more scenarios --- ...flectionClass_newInstanceFromData_001.phpt | 144 ++++++++++++++++-- ...flectionClass_newInstanceFromData_002.phpt | 36 ++++- 2 files changed, 168 insertions(+), 12 deletions(-) diff --git a/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt index 4ebe11226080e..07f1da0dc4f86 100644 --- a/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt @@ -17,7 +17,7 @@ class A class B { public int $a; - public string $b; + public readonly string $b; } class C @@ -29,42 +29,140 @@ class D { } +class E +{ + public function __construct( + public int $a, + public string $b, + ) + {} +} + +class F +{ + public readonly int $a; + + public function __construct( + public readonly string $b, + ) + {} +} + +class G +{ + public readonly int $a; + public readonly string $b; + + public function __construct() + { + $this->b = 456; + } +} + +class H +{ + public int $a { + set(int $value) => $value + 1; + } +} + +class I +{ + public int $a; + + public int $b { + get => $this->a + 1; + set(int $value) { + $this->a = $value - 1; + } + } +} + $rcA = new ReflectionClass('A'); $rcB = new ReflectionClass('B'); $rcC = new ReflectionClass('C'); $rcD = new ReflectionClass('D'); +$rcE = new ReflectionClass('E'); +$rcF = new ReflectionClass('F'); +$rcG = new ReflectionClass('G'); +$rcH = new ReflectionClass('H'); +$rcI = new ReflectionClass('I'); +// assign bad data type to normal class try { $rcA->newInstanceFromData(['a' => 'bad', 'b' => 123], ['foo', 1337]); + echo "you should not see this\n"; } catch(Throwable $e) { echo "Exception: " . $e->getMessage() . "\n"; } +// normal class with constructor var_dump($rcA->newInstanceFromData(['a' => 123, 'b' => 'good'], ['foo', 1337])); +// normal class with no constructor and a readonly property var_dump($rcB->newInstanceFromData(['a' => 123, 'b' => 'good'])); -var_dump($rcC->newInstanceFromData(['a' => 123, 'b' => 'good'])); - -var_dump($rcC->newInstanceFromData([])); +// trying to set dynamic properties on class without AllowDynamicProperties attribute +var_dump($rcC->newInstanceFromData(['a' => 123, 'b' => 'good'])); // this should warn +var_dump($rcC->newInstanceFromData([])); // this is fine +// setting dynamic properties on a class with AllowDynamicProperties attribute var_dump($rcD->newInstanceFromData(['a' => 123, 'b' => 'good'])); +// class with property promotion +try +{ + $rcE->newInstanceFromData(['a' => 123, 'b' => 'good']); // no constructor args will fail + echo "you should not see this\n"; +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + +var_dump($rcE->newInstanceFromData(['a' => 123, 'b' => 'good'], [456, 'foo'])); // constructor args will override class props + +// class with readonly promoted property +var_dump($rcF->newInstanceFromData(['a' => 123], ['b' => 'good'])); + +// readonly property set in the constructor +try +{ + $rcG->newInstanceFromData(['a' => 123, 'b' => 'good']); // setting $b by data will conflict with constructor's set + echo "you should not see this\n"; +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + +// hooked set property +var_dump($rcH->newInstanceFromData(['a' => 1])); + +// virtual property +var_dump($rcI->newInstanceFromData(['a' => 1, 'b' => 2])); + +$instance = $rcI->newInstanceFromData(['a' => 1]); +var_dump($instance); +var_dump($instance->b); +$instance->b = 3; +var_dump($instance->b); + ?> --EXPECTF-- Exception: Cannot assign string to property A::$a of type int In constructor of class A -object(A)#5 (2) { +object(A)#%d (2) { ["a"]=> int(123) ["b"]=> string(4) "good" } -object(B)#5 (2) { +object(B)#%d (2) { ["a"]=> int(123) ["b"]=> @@ -74,17 +172,45 @@ object(B)#5 (2) { Deprecated: Creation of dynamic property C::$a is deprecated in %s on line %d Deprecated: Creation of dynamic property C::$b is deprecated in %s on line %d -object(C)#5 (2) { +object(C)#%d (2) { ["a"]=> int(123) ["b"]=> string(4) "good" } -object(C)#5 (0) { +object(C)#%d (0) { } -object(D)#5 (2) { +object(D)#%d (2) { ["a"]=> int(123) ["b"]=> string(4) "good" } +Exception: Too few arguments to function E::__construct(), 0 passed and exactly 2 expected +object(E)#%d (2) { + ["a"]=> + int(456) + ["b"]=> + string(3) "foo" +} +object(F)#%d (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} +Exception: Cannot modify readonly property G::$b +object(H)#%d (1) { + ["a"]=> + int(2) +} +object(I)#%d (1) { + ["a"]=> + int(1) +} +object(I)#%d (1) { + ["a"]=> + int(1) +} +int(2) +int(3) diff --git a/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt index 0852bbbd55f2b..ef8e8df18add3 100644 --- a/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt @@ -1,10 +1,20 @@ --TEST-- -ReflectionClass::newInstanceFromData - internal class +ReflectionClass::newInstanceFromData - bad instantiations --FILE-- getMessage() . "\n"; } +try +{ + $rcMyInterface->newInstanceFromData(['a' => 123]); +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + +try +{ + $rcMyTrait->newInstanceFromData(['a' => 123]); +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + ?> --EXPECTF-- Exception: Class DateTime is an internal class that cannot be instantiated from data Exception: Class PDOStatement is an internal class that cannot be instantiated from data +Exception: Cannot instantiate interface MyInterface +Exception: Cannot instantiate trait MyTrait From 6af7ab48685f54b62a46b1e311a486e6414f7e5c Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Thu, 14 Aug 2025 22:40:11 +1000 Subject: [PATCH 09/11] add test for class with private constructor which currently triggers an assertion failure --- ...flectionClass_newInstanceFromData_002.phpt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt index ef8e8df18add3..d098477f24993 100644 --- a/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt @@ -16,6 +16,19 @@ $rcMyInterface = new ReflectionClass('MyInterface'); trait MyTrait {} $rcMyTrait = new ReflectionClass('MyTrait'); +// class with private constructor +class A +{ + public int $a; + + private function __construct() + { + echo "In constructor of class A\n"; + } +} + +$rcA = new ReflectionClass('A'); + try { $rcDateTime->newInstanceFromData([], ['now', new DateTimeZone('UTC')]); @@ -52,9 +65,19 @@ catch(Throwable $e) echo "Exception: " . $e->getMessage() . "\n"; } +try +{ + $rcA->newInstanceFromData(['a' => 123]); +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + ?> --EXPECTF-- Exception: Class DateTime is an internal class that cannot be instantiated from data Exception: Class PDOStatement is an internal class that cannot be instantiated from data Exception: Cannot instantiate interface MyInterface Exception: Cannot instantiate trait MyTrait +Exception: Access to non-public constructor of class A From 4d03925d0502a6a5d2463e3c0edb2f18563bc1dd Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Mon, 18 Aug 2025 07:57:36 +1000 Subject: [PATCH 10/11] add test for a promoted readonly property being set in both data and args --- .../ReflectionClass_newInstanceFromData_001.phpt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt index 07f1da0dc4f86..a59b97638c484 100644 --- a/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt @@ -129,6 +129,16 @@ var_dump($rcE->newInstanceFromData(['a' => 123, 'b' => 'good'], [456, 'foo'])); // class with readonly promoted property var_dump($rcF->newInstanceFromData(['a' => 123], ['b' => 'good'])); +try +{ + var_dump($rcF->newInstanceFromData(['a' => 123, 'b' => 'first'], ['b' => 'second'])); + echo "you should not see this\n"; +} +catch(Throwable $e) +{ + echo "Exception: " . $e->getMessage() . "\n"; +} + // readonly property set in the constructor try { @@ -199,6 +209,7 @@ object(F)#%d (2) { ["b"]=> string(4) "good" } +Exception: Cannot modify readonly property F::$b Exception: Cannot modify readonly property G::$b object(H)#%d (1) { ["a"]=> From 8e0e492cc942f9b8779fc4e1aeb75e4b022f599a Mon Sep 17 00:00:00 2001 From: Matt Dwyer Date: Mon, 18 Aug 2025 08:10:00 +1000 Subject: [PATCH 11/11] rejig the code a bit so it validates the object and constructor before setting any values to avoid leaking memory --- ext/reflection/php_reflection.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 39977285e5502..87dd5697b2307 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5164,23 +5164,28 @@ ZEND_METHOD(ReflectionClass, newInstanceFromData) RETURN_THROWS(); } - ZEND_HASH_FOREACH_STR_KEY_VAL(data, key, val) { - zend_update_property_ex(ce, Z_OBJ_P(return_value), key, val); - } ZEND_HASH_FOREACH_END(); - const zend_class_entry *old_scope = EG(fake_scope); EG(fake_scope) = ce; constructor = Z_OBJ_HT_P(return_value)->get_constructor(Z_OBJ_P(return_value)); EG(fake_scope) = old_scope; - /* Run the constructor if there is one */ - if (constructor) { - if (!(constructor->common.fn_flags & ZEND_ACC_PUBLIC)) { - zend_throw_exception_ex(reflection_exception_ptr, 0, "Access to non-public constructor of class %s", ZSTR_VAL(ce->name)); - zval_ptr_dtor(return_value); - RETURN_THROWS(); - } + /* Validate the constructor before we set any property values to avoid leaking memory */ + if (!constructor && argc) { + zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s does not have a constructor, so you cannot pass any constructor arguments", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (constructor && !(constructor->common.fn_flags & ZEND_ACC_PUBLIC)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, "Access to non-public constructor of class %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + /* All good - set the property values and call the constructor if there is one */ + ZEND_HASH_FOREACH_STR_KEY_VAL(data, key, val) { + zend_update_property_ex(ce, Z_OBJ_P(return_value), key, val); + } ZEND_HASH_FOREACH_END(); + if (constructor) { zend_call_known_function( constructor, Z_OBJ_P(return_value), Z_OBJCE_P(return_value), NULL, 0, NULL, args); @@ -5188,9 +5193,6 @@ ZEND_METHOD(ReflectionClass, newInstanceFromData) zend_object_store_ctor_failed(Z_OBJ_P(return_value)); RETURN_THROWS(); } - } else if (argc) { - zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s does not have a constructor, so you cannot pass any constructor arguments", ZSTR_VAL(ce->name)); - RETURN_THROWS(); } } /* }}} */