Commit 6d7cc61e048 for php
commit 6d7cc61e04819b35525789f91290fdba4f1bee13
Author: Gina Peter Banyard <girgias@php.net>
Date: Sat Nov 7 18:06:12 2026 +0000
Zend: fix object_init_with_constructor() with positional args in named_params
Closes GH-25295
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index 8b7fa4c73b3..d8ea2c5ed55 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -1890,19 +1890,21 @@ ZEND_API zend_result object_init_with_constructor(zval *arg, zend_class_entry *c
* 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));
- /* 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;
+ /* As named_arguments may contain positional args we need to check there is actually a string key */
+ ZEND_HASH_FOREACH_STR_KEY(named_params, zend_string *arg_name) {
+ if (arg_name == NULL) {
+ continue;
+ }
+ /* Throw standard Error */
+ zend_throw_error(NULL, "Unknown named parameter $%s", ZSTR_VAL(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;
+ } ZEND_HASH_FOREACH_END();
}
+ 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 */
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index 854dd2979f7..4bd96976a96 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -566,6 +566,28 @@ static ZEND_FUNCTION(zend_object_init_with_constructor)
ZVAL_COPY_VALUE(return_value, &obj);
}
+
+/* Instantiate a class and run the constructor via object_init_with_constructor */
+static ZEND_FUNCTION(zend_object_init_with_constructor_array)
+{
+ zend_class_entry *ce = NULL;
+ HashTable *args;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_CLASS(ce)
+ Z_PARAM_ARRAY_HT(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, 0, NULL, 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 36d1be17a17..785390dd70c 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -287,6 +287,7 @@ 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_object_init_with_constructor_array(string $class, array $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 e2a388659ec..45545edf5db 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/tests/zend_object_init_with_constructor_array.phpt b/ext/zend_test/tests/zend_object_init_with_constructor_array.phpt
new file mode 100644
index 00000000000..0178114abcb
--- /dev/null
+++ b/ext/zend_test/tests/zend_object_init_with_constructor_array.phpt
@@ -0,0 +1,152 @@
+--TEST--
+Zend: Test object_init_with_constructor() API
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+class TestUserWithConstructorArgs {
+ public function __construct(int $int_param, string $string_param) {
+ return new stdClass();
+ }
+ public function __destruct() {
+ echo 'Destructor for ', __CLASS__, PHP_EOL;
+ }
+}
+
+class TestUserWithConstructorNoParams {
+ public function __construct() {
+ return new stdClass();
+ }
+ public function __destruct() {
+ echo 'Destructor for ', __CLASS__, PHP_EOL;
+ }
+}
+
+class TestUserWithoutConstructor {
+ public function __destruct() {
+ echo 'Destructor for ', __CLASS__, PHP_EOL;
+ }
+}
+
+$args = [5, 'str'];
+$argsNamed = [5, 'string_param' => 'str'];
+$argsNamedExtraUnknown = [5, 'string_param' => 'str', 'unknown' => 'unknown'];
+
+echo "Testing param passing\n";
+echo "Test TestUserWithConstructorArgs:\n";
+$o = zend_object_init_with_constructor_array("TestUserWithConstructorArgs", $args);
+var_dump($o);
+unset($o);
+
+$o = zend_object_init_with_constructor_array("TestUserWithConstructorArgs", $argsNamed);
+var_dump($o);
+unset($o);
+
+try {
+ $o = zend_object_init_with_constructor_array("TestUserWithConstructorArgs", $argsNamedExtraUnknown);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "Test TestUserWithConstructorNoParams:\n";
+try {
+ $o = zend_object_init_with_constructor_array("TestUserWithConstructorNoParams", $args);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+ $o = zend_object_init_with_constructor_array("TestUserWithConstructorNoParams", $argsNamed);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+ $o = zend_object_init_with_constructor_array("TestUserWithConstructorNoParams", $argsNamedExtraUnknown);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "Test TestUserWithoutConstructor:\n";
+$o = zend_object_init_with_constructor_array("TestUserWithoutConstructor", $args);
+var_dump($o);
+unset($o);
+
+try {
+ $o = zend_object_init_with_constructor_array("TestUserWithoutConstructor", $argsNamed);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+ $o = zend_object_init_with_constructor_array("TestUserWithoutConstructor", $argsNamedExtraUnknown);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "Test _ZendTestMagicCall (internal class no ctor):\n";
+try {
+ $o = zend_object_init_with_constructor_array("_ZendTestMagicCall", $args);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+ $o = zend_object_init_with_constructor_array("_ZendTestMagicCall", $argsNamed);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+ $o = zend_object_init_with_constructor_array("_ZendTestMagicCall", $argsNamedExtraUnknown);
+ var_dump($o);
+ unset($o);
+} catch (\Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Testing param passing
+Test TestUserWithConstructorArgs:
+object(TestUserWithConstructorArgs)#1 (0) {
+}
+Destructor for TestUserWithConstructorArgs
+object(TestUserWithConstructorArgs)#1 (0) {
+}
+Destructor for TestUserWithConstructorArgs
+Error: Unknown named parameter $unknown
+Test TestUserWithConstructorNoParams:
+object(TestUserWithConstructorNoParams)#1 (0) {
+}
+Destructor for TestUserWithConstructorNoParams
+Error: Unknown named parameter $string_param
+Error: Unknown named parameter $string_param
+Test TestUserWithoutConstructor:
+object(TestUserWithoutConstructor)#3 (0) {
+}
+Destructor for TestUserWithoutConstructor
+Error: Unknown named parameter $string_param
+Error: Unknown named parameter $string_param
+Test _ZendTestMagicCall (internal class no ctor):
+object(_ZendTestMagicCall)#2 (0) {
+}
+Error: Unknown named parameter $string_param
+Error: Unknown named parameter $string_param