diff --git a/Zend/zend_API.c b/Zend/zend_API.c index ae7a06f011761..bc6401c8ca28e 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1846,6 +1846,74 @@ ZEND_API zend_result object_init_ex(zval *arg, zend_class_entry *class_type) /* } /* }}} */ +ZEND_API zend_result object_init_with_constructor(zval *arg, zend_class_entry *class_type, uint32_t param_count, zval *params, HashTable *named_params) /* {{{ */ +{ + zend_result status = _object_and_properties_init(arg, class_type, NULL); + if (UNEXPECTED(status == FAILURE)) { + ZVAL_UNDEF(arg); + return FAILURE; + } + zend_object *obj = Z_OBJ_P(arg); + zend_function *constructor = obj->handlers->get_constructor(obj); + if (constructor == NULL) { + /* The constructor can be NULL for 2 different reasons: + * - It is not defined + * - We are not allowed to call the constructor (e.g. private, or internal opaque class) + * and an exception has been thrown + * in the former case, we are (mostly) done and the object is initialized, + * in the latter we need to destroy the object as initialization failed + */ + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(arg); + ZVAL_UNDEF(arg); + return FAILURE; + } + + /* Surprisingly, this is the only case where internal classes will allow to pass extra arguments + * However, if there are named arguments (and it is not empty), + * an Error must be thrown to be consistent with new ClassName() */ + if (UNEXPECTED(named_params != NULL && zend_hash_num_elements(named_params) != 0)) { + /* Throw standard Error */ + zend_string *arg_name = NULL; + zend_hash_get_current_key(named_params, &arg_name, /* num_index */ NULL); + ZEND_ASSERT(arg_name != NULL); + zend_throw_error(NULL, "Unknown named parameter $%s", ZSTR_VAL(arg_name)); + zend_string_release(arg_name); + /* Do not call destructor, free object, and set arg to IS_UNDEF */ + zend_object_store_ctor_failed(obj); + zval_ptr_dtor(arg); + ZVAL_UNDEF(arg); + return FAILURE; + } else { + return SUCCESS; + } + } + /* A constructor should not return a value, however if an exception is thrown + * zend_call_known_function() will set the retval to IS_UNDEF */ + zval retval; + zend_call_known_function( + constructor, + obj, + class_type, + &retval, + param_count, + params, + named_params + ); + if (Z_TYPE(retval) == IS_UNDEF) { + /* Do not call destructor, free object, and set arg to IS_UNDEF */ + zend_object_store_ctor_failed(obj); + zval_ptr_dtor(arg); + ZVAL_UNDEF(arg); + return FAILURE; + } else { + /* Unlikely, but user constructors may return any value they want */ + zval_ptr_dtor(&retval); + return SUCCESS; + } +} +/* }}} */ + ZEND_API void object_init(zval *arg) /* {{{ */ { ZVAL_OBJ(arg, zend_objects_new(zend_standard_class_def)); diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 90556fcde4245..ab67dd5717e69 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -537,6 +537,7 @@ ZEND_API const char *zend_get_type_by_const(int type); #define array_init_size(arg, size) ZVAL_ARR((arg), zend_new_array(size)) ZEND_API void object_init(zval *arg); ZEND_API zend_result object_init_ex(zval *arg, zend_class_entry *ce); +ZEND_API zend_result object_init_with_constructor(zval *arg, zend_class_entry *class_type, uint32_t param_count, zval *params, HashTable *named_params); ZEND_API zend_result object_and_properties_init(zval *arg, zend_class_entry *ce, HashTable *properties); ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type); ZEND_API void object_properties_init_ex(zend_object *object, HashTable *properties); diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 7eea02cd07d34..d63e89ed42868 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -460,6 +460,29 @@ static ZEND_FUNCTION(zend_call_method) zend_call_method(obj, ce, NULL, ZSTR_VAL(method_name), ZSTR_LEN(method_name), return_value, argc - 2, arg1, arg2); } +/* Instantiate a class and run the constructor via object_init_with_constructor */ +static ZEND_FUNCTION(zend_object_init_with_constructor) +{ + zend_class_entry *ce = NULL; + zval *args; + uint32_t num_args; + HashTable *named_args; + + ZEND_PARSE_PARAMETERS_START(1, -1) + Z_PARAM_CLASS(ce) + Z_PARAM_VARIADIC_WITH_NAMED(args, num_args, named_args) + ZEND_PARSE_PARAMETERS_END(); + + zval obj; + /* We don't use return_value directly to check for memory leaks of the API on failure */ + zend_result status = object_init_with_constructor(&obj, ce, num_args, args, named_args); + if (status == FAILURE) { + RETURN_THROWS(); + } + ZEND_ASSERT(!EG(exception)); + ZVAL_COPY_VALUE(return_value, &obj); +} + static ZEND_FUNCTION(zend_get_unit_enum) { ZEND_PARSE_PARAMETERS_NONE(); diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 5c1bed2847822..8930048b4c20f 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -247,6 +247,8 @@ function zend_get_current_func_name(): string {} function zend_call_method(object|string $obj_or_class, string $method, mixed $arg1 = UNKNOWN, mixed $arg2 = UNKNOWN): mixed {} + function zend_object_init_with_constructor(string $class, mixed ...$args): mixed {} + function zend_test_zend_ini_parse_quantity(string $str): int {} function zend_test_zend_ini_parse_uquantity(string $str): int {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 32229d9d9d323..90740a7a79fdb 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 53027610adee7e1c675b1c1b38886100ce4e63ef */ + * Stub hash: 35c11b9781669cff5ad72aa78b9b3732f7b827f1 */ ZEND_STATIC_ASSERT(PHP_VERSION_ID >= 80000, "test_arginfo.h only supports PHP version ID 80000 or newer, " "but it is included on an older PHP version"); @@ -100,6 +100,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_call_method, 0, 2, IS_MIXED ZEND_ARG_TYPE_INFO(0, arg2, IS_MIXED, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_object_init_with_constructor, 0, 1, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, class, IS_STRING, 0) + ZEND_ARG_VARIADIC_TYPE_INFO(0, args, IS_MIXED, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_zend_ini_parse_quantity, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, str, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -255,6 +260,7 @@ static ZEND_FUNCTION(zend_get_unit_enum); static ZEND_FUNCTION(zend_test_parameter_with_attribute); static ZEND_FUNCTION(zend_get_current_func_name); static ZEND_FUNCTION(zend_call_method); +static ZEND_FUNCTION(zend_object_init_with_constructor); static ZEND_FUNCTION(zend_test_zend_ini_parse_quantity); static ZEND_FUNCTION(zend_test_zend_ini_parse_uquantity); static ZEND_FUNCTION(zend_test_zend_ini_str); @@ -350,6 +356,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(zend_test_parameter_with_attribute, arginfo_zend_test_parameter_with_attribute) ZEND_FE(zend_get_current_func_name, arginfo_zend_get_current_func_name) ZEND_FE(zend_call_method, arginfo_zend_call_method) + ZEND_FE(zend_object_init_with_constructor, arginfo_zend_object_init_with_constructor) ZEND_FE(zend_test_zend_ini_parse_quantity, arginfo_zend_test_zend_ini_parse_quantity) ZEND_FE(zend_test_zend_ini_parse_uquantity, arginfo_zend_test_zend_ini_parse_uquantity) ZEND_FE(zend_test_zend_ini_str, arginfo_zend_test_zend_ini_str) diff --git a/ext/zend_test/tests/zend_object_init_with_constructor.phpt b/ext/zend_test/tests/zend_object_init_with_constructor.phpt new file mode 100644 index 0000000000000..65b111447f0bf --- /dev/null +++ b/ext/zend_test/tests/zend_object_init_with_constructor.phpt @@ -0,0 +1,165 @@ +--TEST-- +Zend: Test object_init_with_constructor() API +--EXTENSIONS-- +zend_test +sysvmsg +--FILE-- +getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("_ZendTestTrait"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("ZendTestUnitEnum"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("AbstractClass"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("SysvMessageQueue"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("PrivateUser"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("ThrowingUser"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "Testing param passing\n"; +try { + $o = zend_object_init_with_constructor("TestUserWithConstructorArgs"); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("TestUserWithConstructorArgs", "str", 5); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $o = zend_object_init_with_constructor("TestUserWithConstructorArgs", 5, string_param: "str", unused_param: 15.3); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +$o = zend_object_init_with_constructor("TestUserWithConstructorArgs", 5, string_param: "str"); +var_dump($o); +unset($o); + +echo "Passing too many args to constructor\n"; +$o = zend_object_init_with_constructor("TestUserWithConstructorArgs", 5, "str", 'unused_param'); +var_dump($o); +unset($o); + +echo "Testing class with defined constructor and no params\n"; +$o = zend_object_init_with_constructor("TestUserWithConstructorNoParams"); +var_dump($o); +unset($o); +?> +--EXPECT-- +Testing impossible initializations +Error: Cannot instantiate interface _ZendTestInterface +Error: Cannot instantiate trait _ZendTestTrait +Error: Cannot instantiate enum ZendTestUnitEnum +Error: Cannot instantiate abstract class AbstractClass +Error: Cannot directly construct SysvMessageQueue, use msg_get_queue() instead +Error: Call to private PrivateUser::__construct() from global scope +Exception: Don't construct +Testing param passing +ArgumentCountError: Too few arguments to function TestUserWithConstructorArgs::__construct(), 0 passed and exactly 2 expected +TypeError: TestUserWithConstructorArgs::__construct(): Argument #1 ($int_param) must be of type int, string given +Error: Unknown named parameter $unused_param +object(TestUserWithConstructorArgs)#1 (0) { +} +Destructor for TestUserWithConstructorArgs +Passing too many args to constructor +object(TestUserWithConstructorArgs)#1 (0) { +} +Destructor for TestUserWithConstructorArgs +Testing class with defined constructor and no params +object(TestUserWithConstructorNoParams)#1 (0) { +} +Destructor for TestUserWithConstructorNoParams diff --git a/ext/zend_test/tests/zend_object_init_with_constructor_classes_without_constructor.phpt b/ext/zend_test/tests/zend_object_init_with_constructor_classes_without_constructor.phpt new file mode 100644 index 0000000000000..1adaaafaa195b --- /dev/null +++ b/ext/zend_test/tests/zend_object_init_with_constructor_classes_without_constructor.phpt @@ -0,0 +1,149 @@ +--TEST-- +Zend: Test object_init_with_constructor() API for objects without constructors +--EXTENSIONS-- +zend_test +--FILE-- +getMessage(), PHP_EOL; +} +echo "Using zend_object_init_with_constructor():\n"; +try { + $o = zend_object_init_with_constructor("_ZendTestMagicCall", 'position_arg'); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\n#### Passing extra named args ####\n"; +echo "Userland class:\n"; +echo "Using new:\n"; +try { + $o = new TestUserWithoutConstructor(unknown_param: 'named_arg'); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +echo "Using zend_object_init_with_constructor():\n"; +try { + $o = zend_object_init_with_constructor("TestUserWithoutConstructor", unknown_param: 'named_arg'); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "Internal class:\n"; +echo "Using new:\n"; +try { + $o = new _ZendTestMagicCall(unknown_param: 'named_arg'); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +echo "Using zend_object_init_with_constructor():\n"; +try { + $o = zend_object_init_with_constructor("_ZendTestMagicCall", unknown_param: 'named_arg'); + var_dump($o); + unset($o); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +#### Passing no args #### +Userland class: +Using new: +object(TestUserWithoutConstructor)#1 (0) { +} +Destructor for TestUserWithoutConstructor +Using zend_object_init_with_constructor(): +object(TestUserWithoutConstructor)#1 (0) { +} +Destructor for TestUserWithoutConstructor +Internal class: +Using new: +object(_ZendTestMagicCall)#1 (0) { +} +Using zend_object_init_with_constructor(): +object(_ZendTestMagicCall)#1 (0) { +} + +#### Passing extra positional args #### +Userland class: +Using new: +object(TestUserWithoutConstructor)#1 (0) { +} +Destructor for TestUserWithoutConstructor +Using zend_object_init_with_constructor(): +object(TestUserWithoutConstructor)#1 (0) { +} +Destructor for TestUserWithoutConstructor +Internal class: +Using new: +object(_ZendTestMagicCall)#1 (0) { +} +Using zend_object_init_with_constructor(): +object(_ZendTestMagicCall)#1 (0) { +} + +#### Passing extra named args #### +Userland class: +Using new: +Error: Unknown named parameter $unknown_param +Using zend_object_init_with_constructor(): +Error: Unknown named parameter $unknown_param +Internal class: +Using new: +Error: Unknown named parameter $unknown_param +Using zend_object_init_with_constructor(): +Error: Unknown named parameter $unknown_param