Skip to content

Commit 27cd7a1

Browse files
committed
Add support for string keys in array unpacking
This adds support for: $array1 = ['a' => 1, 'b' => 2]; $array2 = ['b' => 3, 'c' => 4]; $array = [...$array1, ...$array2]; // => ['a' => 1, 'b' => 3, 'c' => 4] RFC: https://wiki.php.net/rfc/array_unpacking_string_keys Closes GH-6584.
1 parent 3fbd3d2 commit 27cd7a1

File tree

10 files changed

+133
-65
lines changed

10 files changed

+133
-65
lines changed

UPGRADING

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@ PHP 8.1 UPGRADE NOTES
8282

8383
- Core:
8484
. It is now possible to specify octal integer by using the explicit "0o"/"0O"
85-
prefix similar to hexadecimal ("0x"/"0X) and binary ("0b"/"0B") integer literals
85+
prefix similar to hexadecimal ("0x"/"0X) and binary ("0b"/"0B") integer
86+
literals.
8687
RFC: https://wiki.php.net/rfc/explicit_octal_notation
88+
. Added support for array unpacking with strings keys.
89+
RFC: https://wiki.php.net/rfc/array_unpacking_string_keys
8790

8891
- Curl:
8992
. Added CURLOPT_DOH_URL option.

Zend/Optimizer/sccp.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,10 @@ static inline int ct_eval_add_array_unpack(zval *result, zval *array) {
575575
SEPARATE_ARRAY(result);
576576
ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(array), key, value) {
577577
if (key) {
578-
return FAILURE;
578+
value = zend_hash_update(Z_ARR_P(result), key, value);
579+
} else {
580+
value = zend_hash_next_index_insert(Z_ARR_P(result), value);
579581
}
580-
value = zend_hash_next_index_insert(Z_ARR_P(result), value);
581582
if (!value) {
582583
return FAILURE;
583584
}

Zend/Optimizer/zend_inference.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3163,12 +3163,9 @@ static zend_always_inline int _zend_update_type_info(
31633163
case ZEND_ADD_ARRAY_UNPACK:
31643164
tmp = ssa_var_info[ssa_op->result_use].type;
31653165
ZEND_ASSERT(tmp & MAY_BE_ARRAY);
3166-
/* Ignore string keys as they will throw. */
3167-
if (t1 & MAY_BE_ARRAY_KEY_LONG) {
3168-
tmp |= MAY_BE_ARRAY_KEY_LONG | (t1 & (MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF));
3169-
}
3166+
tmp |= t1 & (MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF);
31703167
if (t1 & MAY_BE_OBJECT) {
3171-
tmp |= MAY_BE_ARRAY_KEY_LONG | MAY_BE_ARRAY_OF_ANY;
3168+
tmp |= MAY_BE_ARRAY_KEY_ANY | MAY_BE_ARRAY_OF_ANY;
31723169
}
31733170
UPDATE_SSA_TYPE(tmp, ssa_op->result_def);
31743171
break;

Zend/tests/array_unpack/non_integer_keys.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
Array unpacking does not work with non-integer keys
2+
Array unpacking does not work with non-integer/string keys
33
--FILE--
44
<?php
55
function gen() {
@@ -15,4 +15,4 @@ try {
1515

1616
?>
1717
--EXPECT--
18-
Exception: Cannot unpack Traversable with non-integer keys
18+
Exception: Keys must be of type int|string during array unpacking
Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,58 @@
11
--TEST--
2-
array unpacking with string keys (not supported)
2+
Array unpacking with string keys
33
--FILE--
44
<?php
55

6-
try {
7-
$array = [1, 2, "foo" => 3, 4];
8-
var_dump([...$array]);
9-
} catch (Error $ex) {
10-
var_dump($ex->getMessage());
11-
}
12-
try {
13-
$iterator = new ArrayIterator([1, 2, "foo" => 3, 4]);
14-
var_dump([...$iterator]);
15-
} catch (Error $ex) {
16-
var_dump($ex->getMessage());
6+
// Works with both arrays and Traversables.
7+
$array = [1, 2, "foo" => 3, 4];
8+
var_dump([...$array]);
9+
10+
$iterator = new ArrayIterator([1, 2, "foo" => 3, 4]);
11+
var_dump([...$iterator]);
12+
13+
// Test overwriting behavior.
14+
$array1 = ["foo" => 1];
15+
$array2 = ["foo" => 2];
16+
var_dump(["foo" => 0, ...$array1, ...$array2]);
17+
var_dump(["foo" => 0, ...$array1, ...$array2, "foo" => 3]);
18+
19+
// Test numeric string key from iterator.
20+
function gen() {
21+
yield "42" => 42;
1722
}
23+
var_dump([...gen()]);
1824

1925
?>
2026
--EXPECT--
21-
string(36) "Cannot unpack array with string keys"
22-
string(42) "Cannot unpack Traversable with string keys"
27+
array(4) {
28+
[0]=>
29+
int(1)
30+
[1]=>
31+
int(2)
32+
["foo"]=>
33+
int(3)
34+
[2]=>
35+
int(4)
36+
}
37+
array(4) {
38+
[0]=>
39+
int(1)
40+
[1]=>
41+
int(2)
42+
["foo"]=>
43+
int(3)
44+
[2]=>
45+
int(4)
46+
}
47+
array(1) {
48+
["foo"]=>
49+
int(2)
50+
}
51+
array(1) {
52+
["foo"]=>
53+
int(3)
54+
}
55+
array(1) {
56+
[0]=>
57+
int(42)
58+
}
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
--TEST--
2-
Unpacking of string keys detected at compile-time
2+
Unpacking of string keys is supported at compile-time
33
--FILE--
44
<?php
55

66
var_dump([...['a' => 'b']]);
7+
var_dump(['a' => 'X', ...['a' => 'b']]);
8+
var_dump([...['a' => 'b'], 'a' => 'X']);
79

810
?>
9-
--EXPECTF--
10-
Fatal error: Cannot unpack array with string keys in %s on line %d
11+
--EXPECT--
12+
array(1) {
13+
["a"]=>
14+
string(1) "b"
15+
}
16+
array(1) {
17+
["a"]=>
18+
string(1) "b"
19+
}
20+
array(1) {
21+
["a"]=>
22+
string(1) "X"
23+
}

Zend/zend_ast.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,16 +485,15 @@ static zend_result zend_ast_add_unpacked_element(zval *result, zval *expr) {
485485

486486
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) {
487487
if (key) {
488-
zend_throw_error(NULL, "Cannot unpack array with string keys");
489-
return FAILURE;
488+
zend_hash_update(Z_ARRVAL_P(result), key, val);
490489
} else {
491490
if (!zend_hash_next_index_insert(Z_ARRVAL_P(result), val)) {
492491
zend_throw_error(NULL,
493492
"Cannot add element to the array as the next element is already occupied");
494493
return FAILURE;
495494
}
496-
Z_TRY_ADDREF_P(val);
497495
}
496+
Z_TRY_ADDREF_P(val);
498497
} ZEND_HASH_FOREACH_END();
499498
return SUCCESS;
500499
}

Zend/zend_compile.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8100,9 +8100,8 @@ static bool zend_try_ct_eval_array(zval *result, zend_ast *ast) /* {{{ */
81008100

81018101
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) {
81028102
if (key) {
8103-
zend_error_noreturn(E_COMPILE_ERROR, "Cannot unpack array with string keys");
8104-
}
8105-
if (!zend_hash_next_index_insert(Z_ARRVAL_P(result), val)) {
8103+
zend_hash_update(Z_ARRVAL_P(result), key, val);
8104+
} else if (!zend_hash_next_index_insert(Z_ARRVAL_P(result), val)) {
81068105
zval_ptr_dtor(result);
81078106
return 0;
81088107
}

Zend/zend_vm_def.h

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5952,9 +5952,11 @@ ZEND_VM_HANDLER(147, ZEND_ADD_ARRAY_UNPACK, ANY, ANY)
59525952
{
59535953
USE_OPLINE
59545954
zval *op1;
5955+
HashTable *result_ht;
59555956

59565957
SAVE_OPLINE();
59575958
op1 = GET_OP1_ZVAL_PTR(BP_VAR_R);
5959+
result_ht = Z_ARRVAL_P(EX_VAR(opline->result.var));
59585960

59595961
ZEND_VM_C_LABEL(add_unpack_again):
59605962
if (EXPECTED(Z_TYPE_P(op1) == IS_ARRAY)) {
@@ -5963,16 +5965,14 @@ ZEND_VM_C_LABEL(add_unpack_again):
59635965
zend_string *key;
59645966

59655967
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) {
5968+
if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) {
5969+
val = Z_REFVAL_P(val);
5970+
}
5971+
Z_TRY_ADDREF_P(val);
59665972
if (key) {
5967-
zend_throw_error(NULL, "Cannot unpack array with string keys");
5968-
FREE_OP1();
5969-
HANDLE_EXCEPTION();
5973+
zend_hash_update(result_ht, key, val);
59705974
} else {
5971-
if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) {
5972-
val = Z_REFVAL_P(val);
5973-
}
5974-
Z_TRY_ADDREF_P(val);
5975-
if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) {
5975+
if (!zend_hash_next_index_insert(result_ht, val)) {
59765976
zend_cannot_add_element();
59775977
zval_ptr_dtor_nogc(val);
59785978
break;
@@ -6013,32 +6013,42 @@ ZEND_VM_C_LABEL(add_unpack_again):
60136013
break;
60146014
}
60156015

6016+
zval key;
60166017
if (iter->funcs->get_current_key) {
6017-
zval key;
60186018
iter->funcs->get_current_key(iter, &key);
60196019
if (UNEXPECTED(EG(exception) != NULL)) {
60206020
break;
60216021
}
60226022

6023-
if (UNEXPECTED(Z_TYPE(key) != IS_LONG)) {
6023+
if (UNEXPECTED(Z_TYPE(key) != IS_LONG && Z_TYPE(key) != IS_STRING)) {
60246024
zend_throw_error(NULL,
6025-
(Z_TYPE(key) == IS_STRING) ?
6026-
"Cannot unpack Traversable with string keys" :
6027-
"Cannot unpack Traversable with non-integer keys");
6025+
"Keys must be of type int|string during array unpacking");
60286026
zval_ptr_dtor(&key);
60296027
break;
60306028
}
6029+
} else {
6030+
ZVAL_UNDEF(&key);
60316031
}
60326032

60336033
ZVAL_DEREF(val);
60346034
Z_TRY_ADDREF_P(val);
60356035

6036-
if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) {
6037-
zend_cannot_add_element();
6038-
zval_ptr_dtor_nogc(val);
6036+
zend_ulong num_key;
6037+
if (Z_TYPE(key) == IS_STRING && !ZEND_HANDLE_NUMERIC(Z_STR(key), num_key)) {
6038+
zend_hash_update(result_ht, Z_STR(key), val);
6039+
zval_ptr_dtor_str(&key);
6040+
} else {
6041+
if (!zend_hash_next_index_insert(result_ht, val)) {
6042+
zend_cannot_add_element();
6043+
zval_ptr_dtor_nogc(val);
6044+
break;
6045+
}
60396046
}
60406047

60416048
iter->funcs->move_forward(iter);
6049+
if (UNEXPECTED(EG(exception))) {
6050+
break;
6051+
}
60426052
}
60436053

60446054
zend_iterator_dtor(iter);

Zend/zend_vm_execute.h

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2499,9 +2499,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER(
24992499
{
25002500
USE_OPLINE
25012501
zval *op1;
2502+
HashTable *result_ht;
25022503

25032504
SAVE_OPLINE();
25042505
op1 = get_zval_ptr(opline->op1_type, opline->op1, BP_VAR_R);
2506+
result_ht = Z_ARRVAL_P(EX_VAR(opline->result.var));
25052507

25062508
add_unpack_again:
25072509
if (EXPECTED(Z_TYPE_P(op1) == IS_ARRAY)) {
@@ -2510,16 +2512,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER(
25102512
zend_string *key;
25112513

25122514
ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) {
2515+
if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) {
2516+
val = Z_REFVAL_P(val);
2517+
}
2518+
Z_TRY_ADDREF_P(val);
25132519
if (key) {
2514-
zend_throw_error(NULL, "Cannot unpack array with string keys");
2515-
FREE_OP(opline->op1_type, opline->op1.var);
2516-
HANDLE_EXCEPTION();
2520+
zend_hash_update(result_ht, key, val);
25172521
} else {
2518-
if (Z_ISREF_P(val) && Z_REFCOUNT_P(val) == 1) {
2519-
val = Z_REFVAL_P(val);
2520-
}
2521-
Z_TRY_ADDREF_P(val);
2522-
if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) {
2522+
if (!zend_hash_next_index_insert(result_ht, val)) {
25232523
zend_cannot_add_element();
25242524
zval_ptr_dtor_nogc(val);
25252525
break;
@@ -2560,32 +2560,42 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_ARRAY_UNPACK_SPEC_HANDLER(
25602560
break;
25612561
}
25622562

2563+
zval key;
25632564
if (iter->funcs->get_current_key) {
2564-
zval key;
25652565
iter->funcs->get_current_key(iter, &key);
25662566
if (UNEXPECTED(EG(exception) != NULL)) {
25672567
break;
25682568
}
25692569

2570-
if (UNEXPECTED(Z_TYPE(key) != IS_LONG)) {
2570+
if (UNEXPECTED(Z_TYPE(key) != IS_LONG && Z_TYPE(key) != IS_STRING)) {
25712571
zend_throw_error(NULL,
2572-
(Z_TYPE(key) == IS_STRING) ?
2573-
"Cannot unpack Traversable with string keys" :
2574-
"Cannot unpack Traversable with non-integer keys");
2572+
"Keys must be of type int|string during array unpacking");
25752573
zval_ptr_dtor(&key);
25762574
break;
25772575
}
2576+
} else {
2577+
ZVAL_UNDEF(&key);
25782578
}
25792579

25802580
ZVAL_DEREF(val);
25812581
Z_TRY_ADDREF_P(val);
25822582

2583-
if (!zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), val)) {
2584-
zend_cannot_add_element();
2585-
zval_ptr_dtor_nogc(val);
2583+
zend_ulong num_key;
2584+
if (Z_TYPE(key) == IS_STRING && !ZEND_HANDLE_NUMERIC(Z_STR(key), num_key)) {
2585+
zend_hash_update(result_ht, Z_STR(key), val);
2586+
zval_ptr_dtor_str(&key);
2587+
} else {
2588+
if (!zend_hash_next_index_insert(result_ht, val)) {
2589+
zend_cannot_add_element();
2590+
zval_ptr_dtor_nogc(val);
2591+
break;
2592+
}
25862593
}
25872594

25882595
iter->funcs->move_forward(iter);
2596+
if (UNEXPECTED(EG(exception))) {
2597+
break;
2598+
}
25892599
}
25902600

25912601
zend_iterator_dtor(iter);

0 commit comments

Comments
 (0)