Skip to content

Commit a0502b8

Browse files
Convert numeric keys in object/array casts
RFC: https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts This converts key types as appropriate in object to array and array to object casts, as well as in get_object_vars().
1 parent 531eedf commit a0502b8

14 files changed

+183
-46
lines changed

NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ PHP NEWS
1818
operation). (Dmitry)
1919
. Implemented FR #72768 (Add ENABLE_VIRTUAL_TERMINAL_PROCESSING flag for
2020
php.exe). (Michele Locati)
21+
. Implemented "Convert numeric keys in object/array casts" RFC, fixes
22+
bugs #53838, #61655, #66173, #70925, #72254, etc. (Andrea)
2123

2224
- Date:
2325
. Fixed bug #69587 (DateInterval properties and isset). (jhdxr)

UPGRADING

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ PHP 7.2 UPGRADE NOTES
2525
. is_object() will now return true for objects of class
2626
__PHP_Incomplete_Class.
2727
. Support for Netware operating systems have been removed.
28+
. Casting arrays to objects (with (object) or settype()) will now covert
29+
integer keys to string property names. This fixes the behaviour of previous
30+
versions, where integer keys would become inaccessible properties with
31+
integer names.
32+
. Casting objects to arrays (with (object) or settype()), and retrieving
33+
object properties in an array with get_object_vars(), will now convert
34+
numeric string property names (that is, property names of the format
35+
/^(0|(-?[1-9][0-9]*))$/ where PHP_INT_MIN <= n <= PHP_INT_MAX) to integer
36+
keys. This fixes the behaviour of previous versions, where numeric string
37+
property names would become inaccessible string keys.
2838

2939
========================================
3040
2. New Features

Zend/tests/cast_to_object.phpt

6 Bytes
Binary file not shown.

Zend/tests/object_array_cast.phpt

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ var_dump($obj);
1414
?>
1515
--EXPECT--
1616
object(stdClass)#1 (3) {
17-
[0]=>
17+
["0"]=>
1818
int(1)
19-
[1]=>
19+
["1"]=>
2020
int(2)
21-
[2]=>
21+
["2"]=>
2222
int(3)
2323
}
2424
array(3) {
@@ -38,10 +38,10 @@ array(3) {
3838
int(3)
3939
}
4040
object(stdClass)#1 (3) {
41-
[0]=>
41+
["0"]=>
4242
int(1)
43-
[1]=>
43+
["1"]=>
4444
int(2)
45-
[2]=>
45+
["2"]=>
4646
int(3)
4747
}

Zend/tests/settype_object.phpt

6 Bytes
Binary file not shown.

Zend/zend_builtin_functions.c

+13-6
Original file line numberDiff line numberDiff line change
@@ -1251,12 +1251,9 @@ ZEND_FUNCTION(get_object_vars)
12511251
if (!zobj->ce->default_properties_count && properties == zobj->properties && !ZEND_HASH_GET_APPLY_COUNT(properties)) {
12521252
/* fast copy */
12531253
if (EXPECTED(zobj->handlers == &std_object_handlers)) {
1254-
if (EXPECTED(!(GC_FLAGS(properties) & IS_ARRAY_IMMUTABLE))) {
1255-
GC_REFCOUNT(properties)++;
1256-
}
1257-
RETURN_ARR(properties);
1254+
RETURN_ARR(zend_proptable_to_symtable(properties, 0));
12581255
}
1259-
RETURN_ARR(zend_array_dup(properties));
1256+
RETURN_ARR(zend_proptable_to_symtable(properties, 1));
12601257
} else {
12611258
array_init_size(return_value, zend_hash_num_elements(properties));
12621259

@@ -1273,9 +1270,19 @@ ZEND_FUNCTION(get_object_vars)
12731270
const char *prop_name, *class_name;
12741271
size_t prop_len;
12751272
zend_unmangle_property_name_ex(key, &class_name, &prop_name, &prop_len);
1273+
/* We assume here that a mangled property name is never
1274+
* numeric. This is probably a safe assumption, but
1275+
* theoretically someone might write an extension with
1276+
* private, numeric properties. Well, too bad.
1277+
*/
12761278
zend_hash_str_add_new(Z_ARRVAL_P(return_value), prop_name, prop_len, value);
12771279
} else {
1278-
zend_hash_add_new(Z_ARRVAL_P(return_value), key, value);
1280+
zend_ulong num_key;
1281+
if (ZEND_HANDLE_NUMERIC(key, num_key)) {
1282+
zend_hash_index_add_new(Z_ARRVAL_P(return_value), num_key, value);
1283+
} else {
1284+
zend_hash_add_new(Z_ARRVAL_P(return_value), key, value);
1285+
}
12791286
}
12801287
}
12811288
}

Zend/zend_hash.c

+116
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,122 @@ ZEND_API int ZEND_FASTCALL _zend_handle_numeric_str_ex(const char *key, size_t l
24772477
}
24782478
}
24792479

2480+
/* Takes a "symtable" hashtable (contains integer and non-numeric string keys)
2481+
* and converts it to a "proptable" (contains only string keys).
2482+
* If the symtable didn't need duplicating, its refcount is incremented.
2483+
*/
2484+
ZEND_API HashTable* ZEND_FASTCALL zend_symtable_to_proptable(HashTable *ht)
2485+
{
2486+
zend_ulong num_key;
2487+
zend_string *str_key;
2488+
zval *zv;
2489+
2490+
if (UNEXPECTED(HT_IS_PACKED(ht))) {
2491+
goto convert;
2492+
}
2493+
2494+
ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, zv) {
2495+
if (!str_key) {
2496+
goto convert;
2497+
}
2498+
} ZEND_HASH_FOREACH_END();
2499+
2500+
if (!(GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE)) {
2501+
GC_REFCOUNT(ht)++;
2502+
}
2503+
2504+
return ht;
2505+
2506+
convert:
2507+
{
2508+
HashTable *new_ht = emalloc(sizeof(HashTable));
2509+
2510+
zend_hash_init(new_ht, zend_hash_num_elements(ht), NULL, ZVAL_PTR_DTOR, 0);
2511+
2512+
ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, zv) {
2513+
if (!str_key) {
2514+
str_key = zend_long_to_str(num_key);
2515+
zend_string_delref(str_key);
2516+
}
2517+
do {
2518+
if (Z_OPT_REFCOUNTED_P(zv)) {
2519+
if (Z_ISREF_P(zv) && Z_REFCOUNT_P(zv) == 1) {
2520+
zv = Z_REFVAL_P(zv);
2521+
if (!Z_OPT_REFCOUNTED_P(zv)) {
2522+
break;
2523+
}
2524+
}
2525+
Z_ADDREF_P(zv);
2526+
}
2527+
} while (0);
2528+
zend_hash_update(new_ht, str_key, zv);
2529+
} ZEND_HASH_FOREACH_END();
2530+
2531+
return new_ht;
2532+
}
2533+
}
2534+
2535+
/* Takes a "proptable" hashtable (contains only string keys) and converts it to
2536+
* a "symtable" (contains integer and non-numeric string keys).
2537+
* If the proptable didn't need duplicating, its refcount is incremented.
2538+
*/
2539+
ZEND_API HashTable* ZEND_FASTCALL zend_proptable_to_symtable(HashTable *ht, zend_bool always_duplicate)
2540+
{
2541+
zend_ulong num_key;
2542+
zend_string *str_key;
2543+
zval *zv;
2544+
2545+
ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, zv) {
2546+
/* The `str_key &&` here might seem redundant: property tables should
2547+
* only have string keys. Unfortunately, this isn't true, at the very
2548+
* least because of ArrayObject, which stores a symtable where the
2549+
* property table should be.
2550+
*/
2551+
if (str_key && ZEND_HANDLE_NUMERIC(str_key, num_key)) {
2552+
goto convert;
2553+
}
2554+
} ZEND_HASH_FOREACH_END();
2555+
2556+
if (always_duplicate) {
2557+
return zend_array_dup(ht);
2558+
}
2559+
2560+
if (EXPECTED(!(GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE))) {
2561+
GC_REFCOUNT(ht)++;
2562+
}
2563+
2564+
return ht;
2565+
2566+
convert:
2567+
{
2568+
HashTable *new_ht = emalloc(sizeof(HashTable));
2569+
2570+
zend_hash_init(new_ht, zend_hash_num_elements(ht), NULL, ZVAL_PTR_DTOR, 0);
2571+
2572+
ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, zv) {
2573+
do {
2574+
if (Z_OPT_REFCOUNTED_P(zv)) {
2575+
if (Z_ISREF_P(zv) && Z_REFCOUNT_P(zv) == 1) {
2576+
zv = Z_REFVAL_P(zv);
2577+
if (!Z_OPT_REFCOUNTED_P(zv)) {
2578+
break;
2579+
}
2580+
}
2581+
Z_ADDREF_P(zv);
2582+
}
2583+
} while (0);
2584+
/* Again, thank ArrayObject for `!str_key ||`. */
2585+
if (!str_key || ZEND_HANDLE_NUMERIC(str_key, num_key)) {
2586+
zend_hash_index_update(new_ht, num_key, zv);
2587+
} else {
2588+
zend_hash_update(new_ht, str_key, zv);
2589+
}
2590+
} ZEND_HASH_FOREACH_END();
2591+
2592+
return new_ht;
2593+
}
2594+
}
2595+
24802596
/*
24812597
* Local variables:
24822598
* tab-width: 4

Zend/zend_hash.h

+2
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ ZEND_API uint32_t zend_array_count(HashTable *ht);
247247
ZEND_API HashTable* ZEND_FASTCALL zend_array_dup(HashTable *source);
248248
ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht);
249249
ZEND_API void ZEND_FASTCALL zend_symtable_clean(HashTable *ht);
250+
ZEND_API HashTable* ZEND_FASTCALL zend_symtable_to_proptable(HashTable *ht);
251+
ZEND_API HashTable* ZEND_FASTCALL zend_proptable_to_symtable(HashTable *ht, zend_bool always_duplicate);
250252

251253
ZEND_API int ZEND_FASTCALL _zend_handle_numeric_str_ex(const char *key, size_t length, zend_ulong *idx);
252254

Zend/zend_operators.c

+10-16
Original file line numberDiff line numberDiff line change
@@ -588,25 +588,17 @@ ZEND_API void ZEND_FASTCALL convert_to_array(zval *op) /* {{{ */
588588
if (obj_ht) {
589589
zend_array *arr;
590590

591+
/* fast copy */
591592
if (!Z_OBJCE_P(op)->default_properties_count &&
592593
obj_ht == Z_OBJ_P(op)->properties &&
593-
!ZEND_HASH_GET_APPLY_COUNT(Z_OBJ_P(op)->properties)) {
594-
/* fast copy */
595-
if (EXPECTED(Z_OBJ_P(op)->handlers == &std_object_handlers)) {
596-
arr = obj_ht;
597-
if (EXPECTED(!(GC_FLAGS(Z_OBJ_P(op)->properties) & IS_ARRAY_IMMUTABLE))) {
598-
GC_REFCOUNT(Z_OBJ_P(op)->properties)++;
599-
}
600-
} else {
601-
arr = zend_array_dup(obj_ht);
602-
}
603-
zval_dtor(op);
604-
ZVAL_ARR(op, arr);
594+
!ZEND_HASH_GET_APPLY_COUNT(Z_OBJ_P(op)->properties) &&
595+
EXPECTED(Z_OBJ_P(op)->handlers == &std_object_handlers)) {
596+
arr = zend_proptable_to_symtable(obj_ht, 0);
605597
} else {
606-
arr = zend_array_dup(obj_ht);
607-
zval_dtor(op);
608-
ZVAL_ARR(op, arr);
598+
arr = zend_proptable_to_symtable(obj_ht, 1);
609599
}
600+
zval_dtor(op);
601+
ZVAL_ARR(op, arr);
610602
return;
611603
}
612604
} else {
@@ -645,10 +637,12 @@ ZEND_API void ZEND_FASTCALL convert_to_object(zval *op) /* {{{ */
645637
case IS_ARRAY:
646638
{
647639
HashTable *ht = Z_ARR_P(op);
648-
if (Z_IMMUTABLE_P(op)) {
640+
ht = zend_symtable_to_proptable(ht);
641+
if (GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE) {
649642
/* TODO: try not to duplicate immutable arrays as well ??? */
650643
ht = zend_array_dup(ht);
651644
}
645+
zval_dtor(op);
652646
object_and_properties_init(op, zend_standard_class_def, ht);
653647
break;
654648
}

ext/reflection/tests/bug61388.phpt

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ Array
2424
Array
2525
(
2626
[0] => ReflectionProperty Object
27+
(
28+
[name] => 0
29+
[class] => stdClass
30+
)
31+
32+
[1] => ReflectionProperty Object
2733
(
2834
[name] => oo
2935
[class] => stdClass

ext/spl/tests/arrayObject___construct_basic7.phpt

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ array(2) {
1818
int(1)
1919
}
2020
object(stdClass)#1 (2) {
21-
[1]=>
21+
["1"]=>
2222
int(1)
23-
[0]=>
23+
["0"]=>
2424
int(2)
2525
}
2626
object(ArrayObject)#2 (1) {
2727
["storage":"ArrayObject":private]=>
2828
object(stdClass)#1 (2) {
29-
[1]=>
29+
["1"]=>
3030
int(1)
31-
[0]=>
31+
["0"]=>
3232
int(2)
3333
}
3434
}

ext/standard/tests/array/var_export.phpt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ var_export($a);
77
?>
88
--EXPECT--
99
stdClass::__set_state(array(
10-
0 => 1,
11-
1 => 3,
10+
'0' => 1,
11+
'1' => 3,
1212
'foo' => 'bar',
1313
))

ext/standard/tests/general_functions/gettype_settype_basic.phpt

+6-6
Original file line numberDiff line numberDiff line change
@@ -740,11 +740,11 @@ string(5) "array"
740740
-- Iteration 1 --
741741
bool(true)
742742
object(stdClass)#2 (3) {
743-
[0]=>
743+
["0"]=>
744744
int(1)
745-
[1]=>
745+
["1"]=>
746746
int(2)
747-
[2]=>
747+
["2"]=>
748748
int(3)
749749
}
750750
string(6) "object"
@@ -758,11 +758,11 @@ string(6) "object"
758758
-- Iteration 3 --
759759
bool(true)
760760
object(stdClass)#2 (3) {
761-
[0]=>
761+
["0"]=>
762762
int(2)
763-
[1]=>
763+
["1"]=>
764764
int(3)
765-
[2]=>
765+
["2"]=>
766766
int(4)
767767
}
768768
string(6) "object"

ext/standard/tests/general_functions/type.phpt

+6-6
Original file line numberDiff line numberDiff line change
@@ -265,11 +265,11 @@ array(0) {
265265
}
266266
bool(true)
267267
object(stdClass)#%d (3) {
268-
[0]=>
268+
["0"]=>
269269
int(1)
270-
[1]=>
270+
["1"]=>
271271
int(2)
272-
[2]=>
272+
["2"]=>
273273
int(3)
274274
}
275275
bool(true)
@@ -279,11 +279,11 @@ object(stdClass)#%d (1) {
279279
}
280280
bool(true)
281281
object(stdClass)#%d (3) {
282-
[0]=>
282+
["0"]=>
283283
int(2)
284-
[1]=>
284+
["1"]=>
285285
int(3)
286-
[2]=>
286+
["2"]=>
287287
int(4)
288288
}
289289
bool(true)

0 commit comments

Comments
 (0)