diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index b5ca21d500a8..87dd5697b230 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5132,6 +5132,71 @@ 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); + + 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 + 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_THROWS(); + } + + 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; + + /* 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); + + if (EG(exception)) { + zend_object_store_ctor_failed(Z_OBJ_P(return_value)); + RETURN_THROWS(); + } + } +} +/* }}} */ + 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 7af884953bef..06a1a6054d44 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -371,6 +371,8 @@ public function newInstanceWithoutConstructor(): object {} /** @tentative-return-type */ public function newInstanceArgs(array $args = []): ?object {} + 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 907ada13efab..1f2227a62e3e 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: 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,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_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() + 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 000000000000..a59b97638c48 --- /dev/null +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_001.phpt @@ -0,0 +1,227 @@ +--TEST-- +ReflectionClass::newInstanceFromData +--FILE-- +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'])); + +// 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'])); + +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 +{ + $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)#%d (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} +object(B)#%d (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)#%d (2) { + ["a"]=> + int(123) + ["b"]=> + string(4) "good" +} +object(C)#%d (0) { +} +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 F::$b +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 new file mode 100644 index 000000000000..d098477f2499 --- /dev/null +++ b/ext/reflection/tests/ReflectionClass_newInstanceFromData_002.phpt @@ -0,0 +1,83 @@ +--TEST-- +ReflectionClass::newInstanceFromData - bad instantiations +--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"; +} + +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"; +} + +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 diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index fd5d83e91741..40bc1466ca48 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 = [] ] + } + - Return [ object ] + } + Method [ public method newLazyGhost ] { - Parameters [2] {