Skip to content

Commit dbbf0c8

Browse files
committed
Support named params in call_user_func_array
1 parent 5f5b7bb commit dbbf0c8

File tree

7 files changed

+179
-50
lines changed

7 files changed

+179
-50
lines changed

Zend/tests/named_params/call_user_func.phpt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ $test = function($a = 'a', $b = 'b', $c = 'c') {
99
$test_variadic = function(...$args) {
1010
var_dump($args);
1111
};
12+
$test_ref = function(&$ref) {
13+
$ref++;
14+
};
1215

1316
call_user_func($test, 'A', c: 'C');
1417
call_user_func($test, c: 'C', a: 'A');
1518
call_user_func($test, c: 'C');
1619
call_user_func($test_variadic, 'A', c: 'C');
20+
call_user_func($test_ref, ref: null);
1721
var_dump(call_user_func('call_user_func', $test, c: 'D'));
1822
var_dump(call_user_func('array_slice', [1, 2, 3, 4, 5], length: 2));
1923
echo "\n";
@@ -24,7 +28,7 @@ $test->call(new class {}, 'A', c: 'C');
2428
$test_variadic->call(new class {}, 'A', c: 'C');
2529

2630
?>
27-
--EXPECT--
31+
--EXPECTF--
2832
a = A, b = b, c = C
2933
a = A, b = b, c = C
3034
a = a, b = b, c = C
@@ -34,6 +38,8 @@ array(2) {
3438
["c"]=>
3539
string(1) "C"
3640
}
41+
42+
Warning: {closure}(): Argument #1 ($ref) must be passed by reference, value given in %s on line %d
3743
a = a, b = b, c = D
3844
NULL
3945
array(2) {

Zend/tests/named_params/call_user_func_array.phpt

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,70 @@ Behavior of call_user_func_array() with named parameters
33
--FILE--
44
<?php
55

6-
function test($a, $b) {
7-
var_dump($a, $b);
8-
}
6+
namespace {
7+
$test = function($a = 'a', $b = 'b', $c = 'c') {
8+
echo "a = $a, b = $b, c = $c\n";
9+
};
10+
$test_variadic = function(...$args) {
11+
var_dump($args);
12+
};
913

10-
// Keys are ignored by call_user_func_array().
11-
// Use "..." if you want named params support!
14+
call_user_func_array($test, ['A', 'B']);
15+
call_user_func_array($test, [1 => 'A', 0 => 'B']);
16+
call_user_func_array($test, ['A', 'c' => 'C']);
17+
call_user_func_array($test_variadic, ['A', 'c' => 'C']);
18+
try {
19+
call_user_func_array($test, ['d' => 'D']);
20+
} catch (\Error $e) {
21+
echo $e->getMessage(), "\n";
22+
}
23+
try {
24+
call_user_func_array($test, ['c' => 'C', 'A']);
25+
} catch (\Error $e) {
26+
echo $e->getMessage(), "\n";
27+
}
28+
echo "\n";
29+
}
1230

13-
call_user_func_array('test', [0 => 'a', 1 => 'b']);
14-
call_user_func_array('test', [1 => 'a', 0 => 'b']);
15-
call_user_func_array('test', ['b' => 'a', 'a' => 'b']);
16-
call_user_func_array('test', ['c' => 'a', 'd' => 'b']);
31+
namespace Foo {
32+
call_user_func_array($test, ['A', 'B']);
33+
call_user_func_array($test, [1 => 'A', 0 => 'B']);
34+
call_user_func_array($test, ['A', 'c' => 'C']);
35+
call_user_func_array($test_variadic, ['A', 'c' => 'C']);
36+
try {
37+
call_user_func_array($test, ['d' => 'D']);
38+
} catch (\Error $e) {
39+
echo $e->getMessage(), "\n";
40+
}
41+
try {
42+
call_user_func_array($test, ['c' => 'C', 'A']);
43+
} catch (\Error $e) {
44+
echo $e->getMessage(), "\n";
45+
}
46+
}
1747

1848
?>
1949
--EXPECT--
20-
string(1) "a"
21-
string(1) "b"
22-
string(1) "a"
23-
string(1) "b"
24-
string(1) "a"
25-
string(1) "b"
26-
string(1) "a"
27-
string(1) "b"
50+
a = A, b = B, c = c
51+
a = A, b = B, c = c
52+
a = A, b = b, c = C
53+
array(2) {
54+
[0]=>
55+
string(1) "A"
56+
["c"]=>
57+
string(1) "C"
58+
}
59+
Unknown named parameter $d
60+
Cannot use positional argument after named argument
61+
62+
a = A, b = B, c = c
63+
a = A, b = B, c = c
64+
a = A, b = b, c = C
65+
array(2) {
66+
[0]=>
67+
string(1) "A"
68+
["c"]=>
69+
string(1) "C"
70+
}
71+
Unknown named parameter $d
72+
Cannot use positional argument after named argument

Zend/zend_API.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ typedef struct _zend_fcall_info {
4848
zend_object *object;
4949
zend_bool no_separation;
5050
uint32_t param_count;
51+
/* This hashtable can also contain positional arguments (with integer keys),
52+
* which will be appended to the normal params[]. This makes it easier to
53+
* integrate APIs like call_user_func_array(). The usual restriction that
54+
* there may not be position arguments after named arguments applies. */
5155
HashTable *named_params;
5256
} zend_fcall_info;
5357

Zend/zend_execute_API.c

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,7 @@ int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache) /
753753
zend_param_must_be_ref(func, i + 1);
754754
if (UNEXPECTED(EG(exception))) {
755755
ZEND_CALL_NUM_ARGS(call) = i;
756+
cleanup_args:
756757
zend_vm_stack_free_args(call);
757758
zend_vm_stack_free_call_frame(call);
758759
if (EG(current_execute_data) == &dummy_execute_data) {
@@ -774,27 +775,57 @@ int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache) /
774775
}
775776

776777
if (fci->named_params) {
778+
ZEND_ASSERT(fci->no_separation && "Do we want to support separation?");
779+
777780
zend_string *name;
778-
zval *val;
779-
ZEND_HASH_FOREACH_STR_KEY_VAL(fci->named_params, name, val) {
780-
ZEND_ASSERT(name && "named_params may only contain named params");
781-
782-
void *cache_slot = NULL;
783-
uint32_t arg_num;
784-
zval *target = zend_handle_named_arg(&call, name, &arg_num, &cache_slot);
785-
if (!target) {
786-
zend_vm_stack_free_args(call);
787-
zend_vm_stack_free_call_frame(call);
788-
if (EG(current_execute_data) == &dummy_execute_data) {
789-
EG(current_execute_data) = dummy_execute_data.prev_execute_data;
781+
zval *arg;
782+
uint32_t arg_num = ZEND_CALL_NUM_ARGS(call) + 1;
783+
zend_bool have_named_params = 0;
784+
ZEND_HASH_FOREACH_STR_KEY_VAL(fci->named_params, name, arg) {
785+
zval *target;
786+
if (name) {
787+
void *cache_slot = NULL;
788+
have_named_params = 1;
789+
target = zend_handle_named_arg(&call, name, &arg_num, &cache_slot);
790+
if (!target) {
791+
goto cleanup_args;
790792
}
791-
return FAILURE;
793+
} else {
794+
if (have_named_params) {
795+
zend_throw_error(NULL,
796+
"Cannot use positional argument after named argument");
797+
goto cleanup_args;
798+
}
799+
800+
target = ZEND_CALL_ARG(call, arg_num);
792801
}
793802

794-
/* TODO */
795-
/*if (ARG_SHOULD_BE_SENT_BY_REF(func, arg_num)) {
796-
}*/
797-
ZVAL_COPY(target, val);
803+
if (ARG_SHOULD_BE_SENT_BY_REF(func, arg_num)) {
804+
if (UNEXPECTED(!Z_ISREF_P(arg))) {
805+
if (!fci->no_separation) {
806+
/* TODO ? */
807+
} else if (!ARG_MAY_BE_SENT_BY_REF(func, arg_num)) {
808+
/* By-value send is not allowed -- emit a warning,
809+
* but still perform the call with a by-value send. */
810+
zend_param_must_be_ref(func, arg_num);
811+
if (UNEXPECTED(EG(exception))) {
812+
goto cleanup_args;
813+
}
814+
}
815+
}
816+
} else {
817+
if (Z_ISREF_P(arg) &&
818+
!(func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) {
819+
/* don't separate references for __call */
820+
arg = Z_REFVAL_P(arg);
821+
}
822+
}
823+
824+
ZVAL_COPY(target, arg);
825+
if (!name) {
826+
ZEND_CALL_NUM_ARGS(call)++;
827+
arg_num++;
828+
}
798829
} ZEND_HASH_FOREACH_END();
799830
}
800831

Zend/zend_vm_def.h

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5160,10 +5160,11 @@ ZEND_VM_HANDLER(119, ZEND_SEND_ARRAY, ANY, ANY, NUM)
51605160
HashTable *ht;
51615161
zval *arg, *param;
51625162

5163-
51645163
ZEND_VM_C_LABEL(send_array):
51655164
ht = Z_ARRVAL_P(args);
51665165
if (OP2_TYPE != IS_UNUSED) {
5166+
/* We don't need to handle named params in this case,
5167+
* because array_slice() is called with $preserve_keys == false. */
51675168
zval *op2 = GET_OP2_ZVAL_PTR(BP_VAR_R);
51685169
uint32_t skip = opline->extended_value;
51695170
uint32_t count = zend_hash_num_elements(ht);
@@ -5208,10 +5209,28 @@ ZEND_VM_C_LABEL(send_array):
52085209
}
52095210
FREE_OP2();
52105211
} else {
5212+
zend_string *name;
5213+
zend_bool have_named_params;
52115214
zend_vm_stack_extend_call_frame(&EX(call), 0, zend_hash_num_elements(ht));
52125215
arg_num = 1;
52135216
param = ZEND_CALL_ARG(EX(call), 1);
5214-
ZEND_HASH_FOREACH_VAL(ht, arg) {
5217+
have_named_params = 0;
5218+
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, name, arg) {
5219+
if (name) {
5220+
void *cache_slot = NULL;
5221+
have_named_params = 1;
5222+
param = zend_handle_named_arg(&EX(call), name, &arg_num, &cache_slot);
5223+
if (!param) {
5224+
FREE_OP1();
5225+
HANDLE_EXCEPTION();
5226+
}
5227+
} else if (have_named_params) {
5228+
zend_throw_error(NULL,
5229+
"Cannot use positional argument after named argument");
5230+
FREE_OP1();
5231+
HANDLE_EXCEPTION();
5232+
}
5233+
52155234
if (ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
52165235
if (UNEXPECTED(!Z_ISREF_P(arg))) {
52175236
if (!ARG_MAY_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
@@ -5227,10 +5246,13 @@ ZEND_VM_C_LABEL(send_array):
52275246
arg = Z_REFVAL_P(arg);
52285247
}
52295248
}
5249+
52305250
ZVAL_COPY(param, arg);
5231-
ZEND_CALL_NUM_ARGS(EX(call))++;
5232-
arg_num++;
5233-
param++;
5251+
if (!name) {
5252+
ZEND_CALL_NUM_ARGS(EX(call))++;
5253+
arg_num++;
5254+
param++;
5255+
}
52345256
} ZEND_HASH_FOREACH_END();
52355257
}
52365258
}

Zend/zend_vm_execute.h

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,10 +2105,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_SEND_ARRAY_SPEC_HANDLER(ZEND_O
21052105
HashTable *ht;
21062106
zval *arg, *param;
21072107

2108-
21092108
send_array:
21102109
ht = Z_ARRVAL_P(args);
21112110
if (opline->op2_type != IS_UNUSED) {
2111+
/* We don't need to handle named params in this case,
2112+
* because array_slice() is called with $preserve_keys == false. */
21122113
zval *op2 = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R);
21132114
uint32_t skip = opline->extended_value;
21142115
uint32_t count = zend_hash_num_elements(ht);
@@ -2153,10 +2154,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_SEND_ARRAY_SPEC_HANDLER(ZEND_O
21532154
}
21542155
FREE_OP(opline->op2_type, opline->op2.var);
21552156
} else {
2157+
zend_string *name;
2158+
zend_bool have_named_params;
21562159
zend_vm_stack_extend_call_frame(&EX(call), 0, zend_hash_num_elements(ht));
21572160
arg_num = 1;
21582161
param = ZEND_CALL_ARG(EX(call), 1);
2159-
ZEND_HASH_FOREACH_VAL(ht, arg) {
2162+
have_named_params = 0;
2163+
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, name, arg) {
2164+
if (name) {
2165+
void *cache_slot = NULL;
2166+
have_named_params = 1;
2167+
param = zend_handle_named_arg(&EX(call), name, &arg_num, &cache_slot);
2168+
if (!param) {
2169+
FREE_OP(opline->op1_type, opline->op1.var);
2170+
HANDLE_EXCEPTION();
2171+
}
2172+
} else if (have_named_params) {
2173+
zend_throw_error(NULL,
2174+
"Cannot use positional argument after named argument");
2175+
FREE_OP(opline->op1_type, opline->op1.var);
2176+
HANDLE_EXCEPTION();
2177+
}
2178+
21602179
if (ARG_SHOULD_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
21612180
if (UNEXPECTED(!Z_ISREF_P(arg))) {
21622181
if (!ARG_MAY_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
@@ -2172,10 +2191,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_SEND_ARRAY_SPEC_HANDLER(ZEND_O
21722191
arg = Z_REFVAL_P(arg);
21732192
}
21742193
}
2194+
21752195
ZVAL_COPY(param, arg);
2176-
ZEND_CALL_NUM_ARGS(EX(call))++;
2177-
arg_num++;
2178-
param++;
2196+
if (!name) {
2197+
ZEND_CALL_NUM_ARGS(EX(call))++;
2198+
arg_num++;
2199+
param++;
2200+
}
21792201
} ZEND_HASH_FOREACH_END();
21802202
}
21812203
}

ext/standard/basic_functions.c

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,16 +1589,17 @@ PHP_FUNCTION(call_user_func)
15891589
Warning: This function is special-cased by zend_compile.c and so is usually bypassed */
15901590
PHP_FUNCTION(call_user_func_array)
15911591
{
1592-
zval *params, retval;
1592+
zval retval;
1593+
HashTable *params;
15931594
zend_fcall_info fci;
15941595
zend_fcall_info_cache fci_cache;
15951596

15961597
ZEND_PARSE_PARAMETERS_START(2, 2)
15971598
Z_PARAM_FUNC(fci, fci_cache)
1598-
Z_PARAM_ARRAY(params)
1599+
Z_PARAM_ARRAY_HT(params)
15991600
ZEND_PARSE_PARAMETERS_END();
16001601

1601-
zend_fcall_info_args(&fci, params);
1602+
fci.named_params = params;
16021603
fci.retval = &retval;
16031604

16041605
if (zend_call_function(&fci, &fci_cache) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
@@ -1607,8 +1608,6 @@ PHP_FUNCTION(call_user_func_array)
16071608
}
16081609
ZVAL_COPY_VALUE(return_value, &retval);
16091610
}
1610-
1611-
zend_fcall_info_args_clear(&fci, 1);
16121611
}
16131612
/* }}} */
16141613

0 commit comments

Comments
 (0)