From d2e9a29d63669fbf2e51878265c97b1200438bac Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Fri, 3 Apr 2026 08:00:12 +0200 Subject: [PATCH 01/10] Add array_get and array_has functions # Conflicts: # ext/standard/basic_functions_arginfo.h # ext/standard/basic_functions_decl.h # Conflicts: # ext/standard/basic_functions_arginfo.h # ext/standard/basic_functions_decl.h --- ext/standard/array.c | 108 +++++++++++++++++++++++++ ext/standard/basic_functions.stub.php | 10 +++ ext/standard/basic_functions_arginfo.h | 17 +++- ext/standard/basic_functions_decl.h | 8 +- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index e175b7b8d2f4..9d36f33bed94 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6913,6 +6913,114 @@ PHP_FUNCTION(array_key_exists) } /* }}} */ +/* {{{ Helper function to get a nested value from array using dot notation */ +static zval* array_get_nested(HashTable *ht, const char *key, size_t key_len) +{ + const char *dot; + zval *current; + + /* Find the first dot in the key */ + dot = memchr(key, '.', key_len); + + if (dot == NULL) { + /* No dot found, this is a simple key lookup */ + zend_string *zkey = zend_string_init(key, key_len, 0); + current = zend_symtable_find(ht, zkey); + zend_string_release(zkey); + return current; + } + + /* We have a dot, so we need to recurse */ + size_t segment_len = dot - key; + zend_string *segment = zend_string_init(key, segment_len, 0); + current = zend_symtable_find(ht, segment); + zend_string_release(segment); + + if (current == NULL || Z_TYPE_P(current) != IS_ARRAY) { + return NULL; + } + + /* Recurse into the nested array with the remaining key */ + return array_get_nested(Z_ARRVAL_P(current), dot + 1, key_len - segment_len - 1); +} +/* }}} */ + +/* {{{ Retrieves a value from a deeply nested array using "dot" notation */ +PHP_FUNCTION(array_get) +{ + HashTable *ht; + zval *key = NULL; + zval *default_value = NULL; + zval *result; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ARRAY_HT(ht) + Z_PARAM_ZVAL_OR_NULL(key) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + /* If key is null, return the whole array */ + if (key == NULL || Z_TYPE_P(key) == IS_NULL) { + ZVAL_ARR(return_value, zend_array_dup(ht)); + return; + } + + /* Handle string keys with dot notation */ + if (Z_TYPE_P(key) == IS_STRING) { + result = array_get_nested(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); + + if (result != NULL) { + ZVAL_COPY(return_value, result); + return; + } + } + /* Handle integer keys (no dot notation support) */ + else if (Z_TYPE_P(key) == IS_LONG) { + result = zend_hash_index_find(ht, Z_LVAL_P(key)); + + if (result != NULL) { + ZVAL_COPY(return_value, result); + return; + } + } + + /* Key not found, return default value */ + if (default_value != NULL) { + ZVAL_COPY(return_value, default_value); + } else { + RETVAL_NULL(); + } +} +/* }}} */ + +/* {{{ Checks whether a given item exists in an array using "dot" notation */ +PHP_FUNCTION(array_has) +{ + HashTable *ht; + zval *key; + zval *result; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(ht) + Z_PARAM_ZVAL(key) + ZEND_PARSE_PARAMETERS_END(); + + /* Handle string keys with dot notation */ + if (Z_TYPE_P(key) == IS_STRING) { + result = array_get_nested(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); + RETURN_BOOL(result != NULL); + } + /* Handle integer keys (no dot notation support) */ + else if (Z_TYPE_P(key) == IS_LONG) { + RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); + } + + /* Invalid key type */ + RETURN_FALSE; +} +/* }}} */ + /* {{{ Split array into chunks */ PHP_FUNCTION(array_chunk) { diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 1999c9b92be1..3904ec149d82 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1907,6 +1907,16 @@ function array_key_exists($key, array $array): bool {} */ function key_exists($key, array $array): bool {} +/** + * @compile-time-eval + */ +function array_get(array $array, string|int|null $key = null, mixed $default = null): mixed {} + +/** + * @compile-time-eval + */ +function array_has(array $array, string|int $key): bool {} + /** * @compile-time-eval */ diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index e51a837ffa4d..abcb55d3e2a9 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 36b71aa7bbfe478a5e4af400b2822a77067efa2f + * Stub hash: 484f95966ba7e9f8d0907ef4d81c61435d3ff206 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -376,6 +376,17 @@ ZEND_END_ARG_INFO() #define arginfo_key_exists arginfo_array_key_exists +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_get, 0, 1, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) + ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_LONG|MAY_BE_NULL, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_has, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) + ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_LONG, NULL) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_chunk, 0, 2, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, length, IS_LONG, 0) @@ -2406,6 +2417,8 @@ ZEND_FUNCTION(array_any); ZEND_FUNCTION(array_all); ZEND_FUNCTION(array_map); ZEND_FUNCTION(array_key_exists); +ZEND_FUNCTION(array_get); +ZEND_FUNCTION(array_has); ZEND_FUNCTION(array_chunk); ZEND_FUNCTION(array_combine); ZEND_FUNCTION(array_is_list); @@ -3009,6 +3022,8 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(array_map, arginfo_array_map) ZEND_RAW_FENTRY("array_key_exists", zif_array_key_exists, arginfo_array_key_exists, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("key_exists", zif_array_key_exists, arginfo_key_exists, 0, NULL, NULL) + ZEND_RAW_FENTRY("array_get", zif_array_get, arginfo_array_get, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("array_has", zif_array_has, arginfo_array_has, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("array_chunk", zif_array_chunk, arginfo_array_chunk, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("array_combine", zif_array_combine, arginfo_array_combine, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("array_is_list", zif_array_is_list, arginfo_array_is_list, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index b3eb25c5d988..9592affc36f1 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 36b71aa7bbfe478a5e4af400b2822a77067efa2f */ + * Stub hash: 484f95966ba7e9f8d0907ef4d81c61435d3ff206 */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_36b71aa7bbfe478a5e4af400b2822a77067efa2f_H -#define ZEND_BASIC_FUNCTIONS_DECL_36b71aa7bbfe478a5e4af400b2822a77067efa2f_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_484f95966ba7e9f8d0907ef4d81c61435d3ff206_H +#define ZEND_BASIC_FUNCTIONS_DECL_484f95966ba7e9f8d0907ef4d81c61435d3ff206_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_36b71aa7bbfe478a5e4af400b2822a77067efa2f_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_484f95966ba7e9f8d0907ef4d81c61435d3ff206_H */ From 71343022a0e82c6095401b24f0921a36a5b97f7d Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Fri, 3 Apr 2026 08:30:14 +0200 Subject: [PATCH 02/10] Add tests --- ext/standard/tests/array/array_get.phpt | 63 +++++++++++++++++++++++++ ext/standard/tests/array/array_has.phpt | 60 +++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 ext/standard/tests/array/array_get.phpt create mode 100644 ext/standard/tests/array/array_has.phpt diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt new file mode 100644 index 000000000000..038aa2aa7b2c --- /dev/null +++ b/ext/standard/tests/array/array_get.phpt @@ -0,0 +1,63 @@ +--TEST-- +Test array_get() function +--FILE-- + ['desk' => ['price' => 100]]]; + +// Test nested access with dot notation +var_dump(array_get($array, 'products.desk.price')); + +// Test with default value when key doesn't exist +var_dump(array_get($array, 'products.desk.discount', 0)); + +// Test simple key access +$simple = ['name' => 'John', 'age' => 30]; +var_dump(array_get($simple, 'name')); +var_dump(array_get($simple, 'missing', 'default')); + +// Test with integer key +$indexed = ['a', 'b', 'c']; +var_dump(array_get($indexed, 0)); +var_dump(array_get($indexed, 5, 'not found')); + +// Test with null key (returns whole array) +$test = ['foo' => 'bar']; +var_dump(array_get($test, null)); + +// Test nested with missing intermediate key +var_dump(array_get($array, 'products.chair.price', 50)); + +// Test single level key that doesn't exist +var_dump(array_get($array, 'missing')); + +// Test with numeric string in path (like users.0.name) +$users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; +var_dump(array_get($users, 'users.0.name')); +var_dump(array_get($users, 'users.1.age', 70)); + +echo "Done"; +?> +--EXPECT-- +*** Testing array_get() *** +int(100) +int(0) +string(4) "John" +string(7) "default" +string(1) "a" +string(9) "not found" +array(1) { + ["foo"]=> + string(3) "bar" +} +int(50) +NULL +string(5) "Alice" +int(70) +Done diff --git a/ext/standard/tests/array/array_has.phpt b/ext/standard/tests/array/array_has.phpt new file mode 100644 index 000000000000..1cbe18933d94 --- /dev/null +++ b/ext/standard/tests/array/array_has.phpt @@ -0,0 +1,60 @@ +--TEST-- +Test array_has() function +--FILE-- + ['name' => 'Desk', 'price' => 100]]; + +// Test nested key exists with dot notation +var_dump(array_has($array, 'product.name')); + +// Test nested key doesn't exist +var_dump(array_has($array, 'product.color')); + +// Test intermediate key doesn't exist +var_dump(array_has($array, 'category.name')); + +// Test simple key access +$simple = ['name' => 'John', 'age' => 30]; +var_dump(array_has($simple, 'name')); +var_dump(array_has($simple, 'missing')); + +// Test with integer key +$indexed = ['a', 'b', 'c']; +var_dump(array_has($indexed, 0)); +var_dump(array_has($indexed, 1)); +var_dump(array_has($indexed, 5)); + +// Test with value that is null (key exists, but value is null) +$withNull = ['key' => null]; +var_dump(array_has($withNull, 'key')); + +// Test with numeric string in path (like users.0.name) +$users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; +var_dump(array_has($users, 'users.0.name')); +var_dump(array_has($users, 'users.1.age')); +var_dump(array_has($users, 'users.2.name')); + +echo "Done"; +?> +--EXPECT-- +*** Testing array_has() *** +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +bool(false) +bool(false) +Done From fdfd3b1f854c60e48de8b1cf0a919c69b8e574a9 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Mon, 6 Apr 2026 16:01:48 +0200 Subject: [PATCH 03/10] Accept an array of strings in the $key parameter --- ext/standard/array.c | 138 ++++++++++++++++-------- ext/standard/basic_functions.stub.php | 4 +- ext/standard/tests/array/array_get.phpt | 14 +++ ext/standard/tests/array/array_has.phpt | 14 +++ 4 files changed, 122 insertions(+), 48 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 9d36f33bed94..96ccc0c6399a 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6913,48 +6913,69 @@ PHP_FUNCTION(array_key_exists) } /* }}} */ -/* {{{ Helper function to get a nested value from array using dot notation */ -static zval* array_get_nested(HashTable *ht, const char *key, size_t key_len) +/* {{{ Helper function to get a nested value from array using an array of segments */ +static zval* array_get_nested(HashTable *ht, HashTable *segments) { - const char *dot; + zval *segment_val; zval *current; + HashTable *current_ht; + uint32_t idx; + uint32_t num_segments; - /* Find the first dot in the key */ - dot = memchr(key, '.', key_len); + current_ht = ht; + num_segments = zend_hash_num_elements(segments); - if (dot == NULL) { - /* No dot found, this is a simple key lookup */ - zend_string *zkey = zend_string_init(key, key_len, 0); - current = zend_symtable_find(ht, zkey); - zend_string_release(zkey); - return current; - } + /* Iterate through each segment in the array */ + for (idx = 0; idx < num_segments; idx++) { + /* Get the segment at the current index */ + segment_val = zend_hash_index_find(segments, idx); + + if (segment_val == NULL) { + /* Missing segment in array */ + return NULL; + } + + /* Segment must be a string or int */ + if (Z_TYPE_P(segment_val) == IS_STRING) { + current = zend_symtable_find(current_ht, Z_STR_P(segment_val)); + } else if (Z_TYPE_P(segment_val) == IS_LONG) { + current = zend_hash_index_find(current_ht, Z_LVAL_P(segment_val)); + } else { + /* Invalid segment type */ + return NULL; + } + + /* If this is the last segment, return the result */ + if (idx == num_segments - 1) { + return current; + } - /* We have a dot, so we need to recurse */ - size_t segment_len = dot - key; - zend_string *segment = zend_string_init(key, segment_len, 0); - current = zend_symtable_find(ht, segment); - zend_string_release(segment); + /* Check if the segment exists and is an array for next iteration */ + if (current == NULL || Z_TYPE_P(current) != IS_ARRAY) { + return NULL; + } - if (current == NULL || Z_TYPE_P(current) != IS_ARRAY) { - return NULL; + /* Move to the next level */ + current_ht = Z_ARRVAL_P(current); } - /* Recurse into the nested array with the remaining key */ - return array_get_nested(Z_ARRVAL_P(current), dot + 1, key_len - segment_len - 1); + /* Empty segments array */ + return NULL; } /* }}} */ /* {{{ Retrieves a value from a deeply nested array using "dot" notation */ PHP_FUNCTION(array_get) { - HashTable *ht; + zval *array; zval *key = NULL; zval *default_value = NULL; zval *result; + zval segments_array; + HashTable *ht; ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_ARRAY_HT(ht) + Z_PARAM_ARRAY(array) Z_PARAM_ZVAL_OR_NULL(key) Z_PARAM_OPTIONAL Z_PARAM_ZVAL(default_value) @@ -6962,34 +6983,46 @@ PHP_FUNCTION(array_get) /* If key is null, return the whole array */ if (key == NULL || Z_TYPE_P(key) == IS_NULL) { - ZVAL_ARR(return_value, zend_array_dup(ht)); - return; + RETURN_COPY(array); } - /* Handle string keys with dot notation */ - if (Z_TYPE_P(key) == IS_STRING) { - result = array_get_nested(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); + ht = Z_ARRVAL_P(array); + + /* Handle array keys (array of segments) */ + if (Z_TYPE_P(key) == IS_ARRAY) { + result = array_get_nested(ht, Z_ARRVAL_P(key)); if (result != NULL) { - ZVAL_COPY(return_value, result); - return; + RETURN_COPY(result); } } - /* Handle integer keys (no dot notation support) */ + /* Handle string keys with dot notation - convert to array of segments */ + else if (Z_TYPE_P(key) == IS_STRING) { + /* Use php_explode to split the string by '.' */ + zend_string *delim = ZSTR_CHAR('.'); + array_init(&segments_array); + php_explode(delim, Z_STR_P(key), &segments_array, ZEND_LONG_MAX); + + result = array_get_nested(ht, Z_ARRVAL(segments_array)); + + zval_ptr_dtor(&segments_array); + + if (result != NULL) { + RETURN_COPY(result); + } + } + /* Handle integer keys (simple lookup) */ else if (Z_TYPE_P(key) == IS_LONG) { result = zend_hash_index_find(ht, Z_LVAL_P(key)); if (result != NULL) { - ZVAL_COPY(return_value, result); - return; + RETURN_COPY(result); } } /* Key not found, return default value */ if (default_value != NULL) { - ZVAL_COPY(return_value, default_value); - } else { - RETVAL_NULL(); + RETURN_COPY(default_value); } } /* }}} */ @@ -6997,27 +7030,40 @@ PHP_FUNCTION(array_get) /* {{{ Checks whether a given item exists in an array using "dot" notation */ PHP_FUNCTION(array_has) { - HashTable *ht; + zval *array; zval *key; zval *result; + zval segments_array; + HashTable *ht; ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_ARRAY_HT(ht) + Z_PARAM_ARRAY(array) Z_PARAM_ZVAL(key) ZEND_PARSE_PARAMETERS_END(); - /* Handle string keys with dot notation */ - if (Z_TYPE_P(key) == IS_STRING) { - result = array_get_nested(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); + ht = Z_ARRVAL_P(array); + + /* Handle array keys (array of segments) */ + if (Z_TYPE_P(key) == IS_ARRAY) { + result = array_get_nested(ht, Z_ARRVAL_P(key)); RETURN_BOOL(result != NULL); } - /* Handle integer keys (no dot notation support) */ - else if (Z_TYPE_P(key) == IS_LONG) { - RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); + /* Handle string keys with dot notation - convert to array of segments */ + if (Z_TYPE_P(key) == IS_STRING) { + /* Use php_explode to split the string by '.' */ + zend_string *delim = ZSTR_CHAR('.'); + array_init(&segments_array); + php_explode(delim, Z_STR_P(key), &segments_array, ZEND_LONG_MAX); + + result = array_get_nested(ht, Z_ARRVAL(segments_array)); + + zval_ptr_dtor(&segments_array); + RETURN_BOOL(result != NULL); } - /* Invalid key type */ - RETURN_FALSE; + /* Handle integer keys (simple lookup) */ + ZEND_ASSERT(Z_TYPE_P(key) == IS_LONG); + RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); } /* }}} */ diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 3904ec149d82..82a3649caf56 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1910,12 +1910,12 @@ function key_exists($key, array $array): bool {} /** * @compile-time-eval */ -function array_get(array $array, string|int|null $key = null, mixed $default = null): mixed {} +function array_get(array $array, string|int|array|null $key = null, mixed $default = null): mixed {} /** * @compile-time-eval */ -function array_has(array $array, string|int $key): bool {} +function array_has(array $array, string|int|array $key): bool {} /** * @compile-time-eval diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt index 038aa2aa7b2c..00678ff0bd02 100644 --- a/ext/standard/tests/array/array_get.phpt +++ b/ext/standard/tests/array/array_get.phpt @@ -42,6 +42,15 @@ $users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; var_dump(array_get($users, 'users.0.name')); var_dump(array_get($users, 'users.1.age', 70)); +// Test with array key (equivalent to dot notation) +var_dump(array_get($array, ['products', 'desk', 'price'])); +var_dump(array_get($simple, ['name'])); +var_dump(array_get($users, ['users', 0, 'name'])); +var_dump(array_get($array, ['products', 'chair', 'price'], 75)); + +// Test with invalid segment type in array key +var_dump(array_get($array, ['products', new stdClass(), 'price'], 'invalid')); + echo "Done"; ?> --EXPECT-- @@ -60,4 +69,9 @@ int(50) NULL string(5) "Alice" int(70) +int(100) +string(4) "John" +string(5) "Alice" +int(75) +string(7) "invalid" Done diff --git a/ext/standard/tests/array/array_has.phpt b/ext/standard/tests/array/array_has.phpt index 1cbe18933d94..14fbe1598b40 100644 --- a/ext/standard/tests/array/array_has.phpt +++ b/ext/standard/tests/array/array_has.phpt @@ -41,6 +41,15 @@ var_dump(array_has($users, 'users.0.name')); var_dump(array_has($users, 'users.1.age')); var_dump(array_has($users, 'users.2.name')); +// Test with array key (equivalent to dot notation) +var_dump(array_has($array, ['product', 'name'])); +var_dump(array_has($simple, ['name'])); +var_dump(array_has($users, ['users', 0, 'name'])); +var_dump(array_has($array, ['product', 'missing'])); + +// Test with invalid segment type in array key +var_dump(array_has($array, ['product', new stdClass()])); + echo "Done"; ?> --EXPECT-- @@ -57,4 +66,9 @@ bool(true) bool(true) bool(false) bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) Done From 37495c11f80868bd66a179d170af1e82ae2e2890 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Tue, 7 Apr 2026 19:31:26 +0200 Subject: [PATCH 04/10] changes after new PR review --- ext/standard/array.c | 143 ++++++++++++++++-------- ext/standard/tests/array/array_get.phpt | 20 ++++ 2 files changed, 116 insertions(+), 47 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 96ccc0c6399a..ff8ebbad7613 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6914,7 +6914,7 @@ PHP_FUNCTION(array_key_exists) /* }}} */ /* {{{ Helper function to get a nested value from array using an array of segments */ -static zval* array_get_nested(HashTable *ht, HashTable *segments) +static zval* array_get_nested_from_hash(HashTable *ht, HashTable *segments) { zval *segment_val; zval *current; @@ -6964,6 +6964,58 @@ static zval* array_get_nested(HashTable *ht, HashTable *segments) } /* }}} */ +/* {{{ Helper function to get a nested value from array using dot notation string */ +static zval* array_get_nested_from_string(HashTable *ht, const char *key, size_t key_len) +{ + const char *segment_start; + const char *dot; + size_t segment_len; + size_t remaining_len; + zval *current; + HashTable *current_ht; + zend_string *segment; + + current_ht = ht; + segment_start = key; + remaining_len = key_len; + + /* Iterate through each dot-separated segment */ + while (remaining_len > 0) { + /* Find the next dot */ + dot = memchr(segment_start, '.', remaining_len); + + if (dot == NULL) { + /* Last segment */ + segment_len = remaining_len; + } else { + segment_len = dot - segment_start; + } + + /* Look up the current segment */ + segment = zend_string_init(segment_start, segment_len, 0); + current = zend_symtable_find(current_ht, segment); + zend_string_release(segment); + + /* If this is the last segment, return the result */ + if (dot == NULL) { + return current; + } + + /* Check if the segment exists and is an array for next iteration */ + if (current == NULL || Z_TYPE_P(current) != IS_ARRAY) { + return NULL; + } + + /* Move to the next segment */ + current_ht = Z_ARRVAL_P(current); + segment_start = dot + 1; + remaining_len = remaining_len - segment_len - 1; + } + + return NULL; +} +/* }}} */ + /* {{{ Retrieves a value from a deeply nested array using "dot" notation */ PHP_FUNCTION(array_get) { @@ -6971,7 +7023,6 @@ PHP_FUNCTION(array_get) zval *key = NULL; zval *default_value = NULL; zval *result; - zval segments_array; HashTable *ht; ZEND_PARSE_PARAMETERS_START(2, 3) @@ -6988,41 +7039,42 @@ PHP_FUNCTION(array_get) ht = Z_ARRVAL_P(array); - /* Handle array keys (array of segments) */ - if (Z_TYPE_P(key) == IS_ARRAY) { - result = array_get_nested(ht, Z_ARRVAL_P(key)); + switch (Z_TYPE_P(key)) { + case IS_ARRAY: + /* Handle array keys (array of segments) */ + result = array_get_nested_from_hash(ht, Z_ARRVAL_P(key)); - if (result != NULL) { - RETURN_COPY(result); - } - } - /* Handle string keys with dot notation - convert to array of segments */ - else if (Z_TYPE_P(key) == IS_STRING) { - /* Use php_explode to split the string by '.' */ - zend_string *delim = ZSTR_CHAR('.'); - array_init(&segments_array); - php_explode(delim, Z_STR_P(key), &segments_array, ZEND_LONG_MAX); + if (result != NULL) { + RETURN_COPY_DEREF(result); + } + break; - result = array_get_nested(ht, Z_ARRVAL(segments_array)); + case IS_STRING: + /* Handle string keys with dot notation */ + result = array_get_nested_from_string(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); - zval_ptr_dtor(&segments_array); + if (result != NULL) { + RETURN_COPY_DEREF(result); + } + break; - if (result != NULL) { - RETURN_COPY(result); - } - } - /* Handle integer keys (simple lookup) */ - else if (Z_TYPE_P(key) == IS_LONG) { - result = zend_hash_index_find(ht, Z_LVAL_P(key)); + case IS_LONG: + /* Handle integer keys (simple lookup) */ + result = zend_hash_index_find(ht, Z_LVAL_P(key)); - if (result != NULL) { - RETURN_COPY(result); - } + if (result != NULL) { + RETURN_COPY_DEREF(result); + } + break; + + default: + zend_argument_type_error(2, "must be of type string|int|array, %s given", zend_zval_value_name(key)); + RETURN_THROWS(); } /* Key not found, return default value */ if (default_value != NULL) { - RETURN_COPY(default_value); + RETURN_COPY_DEREF(default_value); } } /* }}} */ @@ -7033,7 +7085,6 @@ PHP_FUNCTION(array_has) zval *array; zval *key; zval *result; - zval segments_array; HashTable *ht; ZEND_PARSE_PARAMETERS_START(2, 2) @@ -7043,27 +7094,25 @@ PHP_FUNCTION(array_has) ht = Z_ARRVAL_P(array); - /* Handle array keys (array of segments) */ - if (Z_TYPE_P(key) == IS_ARRAY) { - result = array_get_nested(ht, Z_ARRVAL_P(key)); - RETURN_BOOL(result != NULL); - } - /* Handle string keys with dot notation - convert to array of segments */ - if (Z_TYPE_P(key) == IS_STRING) { - /* Use php_explode to split the string by '.' */ - zend_string *delim = ZSTR_CHAR('.'); - array_init(&segments_array); - php_explode(delim, Z_STR_P(key), &segments_array, ZEND_LONG_MAX); + switch (Z_TYPE_P(key)) { + case IS_ARRAY: + /* Handle array keys (array of segments) */ + result = array_get_nested_from_hash(ht, Z_ARRVAL_P(key)); + RETURN_BOOL(result != NULL); - result = array_get_nested(ht, Z_ARRVAL(segments_array)); + case IS_STRING: + /* Handle string keys with dot notation */ + result = array_get_nested_from_string(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); + RETURN_BOOL(result != NULL); - zval_ptr_dtor(&segments_array); - RETURN_BOOL(result != NULL); - } + case IS_LONG: + /* Handle integer keys (simple lookup) */ + RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); - /* Handle integer keys (simple lookup) */ - ZEND_ASSERT(Z_TYPE_P(key) == IS_LONG); - RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); + default: + zend_argument_type_error(2, "must be of type string|int|array, %s given", zend_zval_value_name(key)); + RETURN_THROWS(); + } } /* }}} */ diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt index 00678ff0bd02..fd1c54363756 100644 --- a/ext/standard/tests/array/array_get.phpt +++ b/ext/standard/tests/array/array_get.phpt @@ -51,6 +51,22 @@ var_dump(array_get($array, ['products', 'chair', 'price'], 75)); // Test with invalid segment type in array key var_dump(array_get($array, ['products', new stdClass(), 'price'], 'invalid')); +// Test with references - ensure returned value is a copy, not a reference +$ref_array = ['data' => ['value' => 'original']]; +$ref =& $ref_array['data']['value']; +$result = array_get($ref_array, 'data.value'); +var_dump($result); +$ref = 'modified'; +var_dump($result); // Should still be 'original' (not affected by reference change) + +// Test with default value being a reference +$default_value = 'default'; +$default_ref =& $default_value; +$result_with_ref_default = array_get($ref_array, 'missing.key', $default_ref); +var_dump($result_with_ref_default); +$default_value = 'changed'; +var_dump($result_with_ref_default); // Should still be 'default' (not affected by reference change) + echo "Done"; ?> --EXPECT-- @@ -74,4 +90,8 @@ string(4) "John" string(5) "Alice" int(75) string(7) "invalid" +string(8) "original" +string(8) "original" +string(7) "default" +string(7) "default" Done From 8f494a02ebafc5dc8b43f700b7b18470d152c85a Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Sat, 11 Apr 2026 11:53:39 +0200 Subject: [PATCH 05/10] Remove dot notation --- ext/standard/array.c | 142 +++--------------------- ext/standard/basic_functions.stub.php | 4 +- ext/standard/tests/array/array_get.phpt | 65 +++-------- ext/standard/tests/array/array_has.phpt | 51 +++------ 4 files changed, 52 insertions(+), 210 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index ff8ebbad7613..2ee974a9abb5 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6913,8 +6913,8 @@ PHP_FUNCTION(array_key_exists) } /* }}} */ -/* {{{ Helper function to get a nested value from array using an array of segments */ -static zval* array_get_nested_from_hash(HashTable *ht, HashTable *segments) +/* {{{ Helper function to get a nested value from array using an array of path segments */ +static zval* array_get_nested(HashTable *ht, HashTable *path) { zval *segment_val; zval *current; @@ -6923,12 +6923,12 @@ static zval* array_get_nested_from_hash(HashTable *ht, HashTable *segments) uint32_t num_segments; current_ht = ht; - num_segments = zend_hash_num_elements(segments); + num_segments = zend_hash_num_elements(path); - /* Iterate through each segment in the array */ + /* Iterate through each segment in the path array */ for (idx = 0; idx < num_segments; idx++) { /* Get the segment at the current index */ - segment_val = zend_hash_index_find(segments, idx); + segment_val = zend_hash_index_find(path, idx); if (segment_val == NULL) { /* Missing segment in array */ @@ -6959,160 +6959,54 @@ static zval* array_get_nested_from_hash(HashTable *ht, HashTable *segments) current_ht = Z_ARRVAL_P(current); } - /* Empty segments array */ + /* Empty path array */ return NULL; } /* }}} */ -/* {{{ Helper function to get a nested value from array using dot notation string */ -static zval* array_get_nested_from_string(HashTable *ht, const char *key, size_t key_len) -{ - const char *segment_start; - const char *dot; - size_t segment_len; - size_t remaining_len; - zval *current; - HashTable *current_ht; - zend_string *segment; - - current_ht = ht; - segment_start = key; - remaining_len = key_len; - - /* Iterate through each dot-separated segment */ - while (remaining_len > 0) { - /* Find the next dot */ - dot = memchr(segment_start, '.', remaining_len); - - if (dot == NULL) { - /* Last segment */ - segment_len = remaining_len; - } else { - segment_len = dot - segment_start; - } - - /* Look up the current segment */ - segment = zend_string_init(segment_start, segment_len, 0); - current = zend_symtable_find(current_ht, segment); - zend_string_release(segment); - - /* If this is the last segment, return the result */ - if (dot == NULL) { - return current; - } - - /* Check if the segment exists and is an array for next iteration */ - if (current == NULL || Z_TYPE_P(current) != IS_ARRAY) { - return NULL; - } - - /* Move to the next segment */ - current_ht = Z_ARRVAL_P(current); - segment_start = dot + 1; - remaining_len = remaining_len - segment_len - 1; - } - - return NULL; -} -/* }}} */ - -/* {{{ Retrieves a value from a deeply nested array using "dot" notation */ +/* {{{ Retrieves a value from a deeply nested array using an array path */ PHP_FUNCTION(array_get) { zval *array; - zval *key = NULL; + zval *path; zval *default_value = NULL; zval *result; - HashTable *ht; ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_ARRAY(array) - Z_PARAM_ZVAL_OR_NULL(key) + Z_PARAM_ARRAY(path) Z_PARAM_OPTIONAL Z_PARAM_ZVAL(default_value) ZEND_PARSE_PARAMETERS_END(); - /* If key is null, return the whole array */ - if (key == NULL || Z_TYPE_P(key) == IS_NULL) { - RETURN_COPY(array); - } - - ht = Z_ARRVAL_P(array); - - switch (Z_TYPE_P(key)) { - case IS_ARRAY: - /* Handle array keys (array of segments) */ - result = array_get_nested_from_hash(ht, Z_ARRVAL_P(key)); - - if (result != NULL) { - RETURN_COPY_DEREF(result); - } - break; - - case IS_STRING: - /* Handle string keys with dot notation */ - result = array_get_nested_from_string(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); - - if (result != NULL) { - RETURN_COPY_DEREF(result); - } - break; - - case IS_LONG: - /* Handle integer keys (simple lookup) */ - result = zend_hash_index_find(ht, Z_LVAL_P(key)); - - if (result != NULL) { - RETURN_COPY_DEREF(result); - } - break; + result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); - default: - zend_argument_type_error(2, "must be of type string|int|array, %s given", zend_zval_value_name(key)); - RETURN_THROWS(); + if (result != NULL) { + RETURN_COPY_DEREF(result); } - /* Key not found, return default value */ + /* Path not found, return default value */ if (default_value != NULL) { RETURN_COPY_DEREF(default_value); } } /* }}} */ -/* {{{ Checks whether a given item exists in an array using "dot" notation */ +/* {{{ Checks whether a given item exists in an array using an array path */ PHP_FUNCTION(array_has) { zval *array; - zval *key; + zval *path; zval *result; - HashTable *ht; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_ARRAY(array) - Z_PARAM_ZVAL(key) + Z_PARAM_ARRAY(path) ZEND_PARSE_PARAMETERS_END(); - ht = Z_ARRVAL_P(array); - - switch (Z_TYPE_P(key)) { - case IS_ARRAY: - /* Handle array keys (array of segments) */ - result = array_get_nested_from_hash(ht, Z_ARRVAL_P(key)); - RETURN_BOOL(result != NULL); - - case IS_STRING: - /* Handle string keys with dot notation */ - result = array_get_nested_from_string(ht, Z_STRVAL_P(key), Z_STRLEN_P(key)); - RETURN_BOOL(result != NULL); - - case IS_LONG: - /* Handle integer keys (simple lookup) */ - RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); + result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); - default: - zend_argument_type_error(2, "must be of type string|int|array, %s given", zend_zval_value_name(key)); - RETURN_THROWS(); - } + RETURN_BOOL(result != NULL); } /* }}} */ diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 82a3649caf56..966e7a717696 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1910,12 +1910,12 @@ function key_exists($key, array $array): bool {} /** * @compile-time-eval */ -function array_get(array $array, string|int|array|null $key = null, mixed $default = null): mixed {} +function array_get(array $array, array $path, mixed $default = null): mixed {} /** * @compile-time-eval */ -function array_has(array $array, string|int|array $key): bool {} +function array_has(array $array, array $path): bool {} /** * @compile-time-eval diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt index fd1c54363756..44eb391f0282 100644 --- a/ext/standard/tests/array/array_get.phpt +++ b/ext/standard/tests/array/array_get.phpt @@ -2,59 +2,40 @@ Test array_get() function --FILE-- ['desk' => ['price' => 100]]]; -// Test nested access with dot notation -var_dump(array_get($array, 'products.desk.price')); +// Test nested access with array path +var_dump(array_get($array, ['products', 'desk', 'price'])); -// Test with default value when key doesn't exist -var_dump(array_get($array, 'products.desk.discount', 0)); +// Test with default value when path doesn't exist +var_dump(array_get($array, ['products', 'desk', 'discount'], 5)); -// Test simple key access +// Test simple path with single level $simple = ['name' => 'John', 'age' => 30]; -var_dump(array_get($simple, 'name')); -var_dump(array_get($simple, 'missing', 'default')); - -// Test with integer key -$indexed = ['a', 'b', 'c']; -var_dump(array_get($indexed, 0)); -var_dump(array_get($indexed, 5, 'not found')); - -// Test with null key (returns whole array) -$test = ['foo' => 'bar']; -var_dump(array_get($test, null)); - -// Test nested with missing intermediate key -var_dump(array_get($array, 'products.chair.price', 50)); +var_dump(array_get($simple, ['name'])); +var_dump(array_get($simple, ['missing'], 'default')); // Test single level key that doesn't exist -var_dump(array_get($array, 'missing')); +var_dump(array_get($array, ['missing'])); -// Test with numeric string in path (like users.0.name) +// Test with integer key in path $users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; -var_dump(array_get($users, 'users.0.name')); -var_dump(array_get($users, 'users.1.age', 70)); - -// Test with array key (equivalent to dot notation) -var_dump(array_get($array, ['products', 'desk', 'price'])); -var_dump(array_get($simple, ['name'])); var_dump(array_get($users, ['users', 0, 'name'])); +var_dump(array_get($users, ['users', 1, 'name'])); + +// Test nested with missing intermediate key var_dump(array_get($array, ['products', 'chair', 'price'], 75)); -// Test with invalid segment type in array key +// Test with invalid segment type in array path var_dump(array_get($array, ['products', new stdClass(), 'price'], 'invalid')); // Test with references - ensure returned value is a copy, not a reference $ref_array = ['data' => ['value' => 'original']]; $ref =& $ref_array['data']['value']; -$result = array_get($ref_array, 'data.value'); +$result = array_get($ref_array, ['data', 'value']); var_dump($result); $ref = 'modified'; var_dump($result); // Should still be 'original' (not affected by reference change) @@ -62,7 +43,7 @@ var_dump($result); // Should still be 'original' (not affected by reference chan // Test with default value being a reference $default_value = 'default'; $default_ref =& $default_value; -$result_with_ref_default = array_get($ref_array, 'missing.key', $default_ref); +$result_with_ref_default = array_get($ref_array, ['missing', 'key'], $default_ref); var_dump($result_with_ref_default); $default_value = 'changed'; var_dump($result_with_ref_default); // Should still be 'default' (not affected by reference change) @@ -72,22 +53,12 @@ echo "Done"; --EXPECT-- *** Testing array_get() *** int(100) -int(0) +int(5) string(4) "John" string(7) "default" -string(1) "a" -string(9) "not found" -array(1) { - ["foo"]=> - string(3) "bar" -} -int(50) NULL string(5) "Alice" -int(70) -int(100) -string(4) "John" -string(5) "Alice" +string(3) "Bob" int(75) string(7) "invalid" string(8) "original" diff --git a/ext/standard/tests/array/array_has.phpt b/ext/standard/tests/array/array_has.phpt index 14fbe1598b40..197a2ea277fa 100644 --- a/ext/standard/tests/array/array_has.phpt +++ b/ext/standard/tests/array/array_has.phpt @@ -2,52 +2,36 @@ Test array_has() function --FILE-- ['name' => 'Desk', 'price' => 100]]; -// Test nested key exists with dot notation -var_dump(array_has($array, 'product.name')); +// Test nested key exists with array path +var_dump(array_has($array, ['product', 'name'])); // Test nested key doesn't exist -var_dump(array_has($array, 'product.color')); +var_dump(array_has($array, ['product', 'color'])); // Test intermediate key doesn't exist -var_dump(array_has($array, 'category.name')); +var_dump(array_has($array, ['category', 'name'])); -// Test simple key access +// Test simple path with single level $simple = ['name' => 'John', 'age' => 30]; -var_dump(array_has($simple, 'name')); -var_dump(array_has($simple, 'missing')); +var_dump(array_has($simple, ['name'])); +var_dump(array_has($simple, ['missing'])); -// Test with integer key -$indexed = ['a', 'b', 'c']; -var_dump(array_has($indexed, 0)); -var_dump(array_has($indexed, 1)); -var_dump(array_has($indexed, 5)); +// Test with integer key in path +$users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; +var_dump(array_has($users, ['users', 0, 'name'])); +var_dump(array_has($users, ['users', 1, 'name'])); +var_dump(array_has($users, ['users', 2, 'name'])); // Test with value that is null (key exists, but value is null) $withNull = ['key' => null]; -var_dump(array_has($withNull, 'key')); - -// Test with numeric string in path (like users.0.name) -$users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; -var_dump(array_has($users, 'users.0.name')); -var_dump(array_has($users, 'users.1.age')); -var_dump(array_has($users, 'users.2.name')); - -// Test with array key (equivalent to dot notation) -var_dump(array_has($array, ['product', 'name'])); -var_dump(array_has($simple, ['name'])); -var_dump(array_has($users, ['users', 0, 'name'])); -var_dump(array_has($array, ['product', 'missing'])); +var_dump(array_has($withNull, ['key'])); -// Test with invalid segment type in array key +// Test with invalid segment type in array path var_dump(array_has($array, ['product', new stdClass()])); echo "Done"; @@ -63,12 +47,5 @@ bool(true) bool(true) bool(false) bool(true) -bool(true) -bool(false) -bool(false) -bool(true) -bool(true) -bool(true) -bool(false) bool(false) Done From c2b7d0361175d1646a73581d681b83d1f87b43fc Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Sat, 11 Apr 2026 18:25:38 +0200 Subject: [PATCH 06/10] More changes after PR review --- ext/standard/array.c | 55 ++++++++++++------------- ext/standard/tests/array/array_get.phpt | 13 ++++++ ext/standard/tests/array/array_has.phpt | 13 ++++++ 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 2ee974a9abb5..80638599c57d 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6917,23 +6917,17 @@ PHP_FUNCTION(array_key_exists) static zval* array_get_nested(HashTable *ht, HashTable *path) { zval *segment_val; - zval *current; - HashTable *current_ht; - uint32_t idx; - uint32_t num_segments; - - current_ht = ht; - num_segments = zend_hash_num_elements(path); + zval *current = NULL; + HashTable *current_ht = ht; + uint32_t num_segments = zend_hash_num_elements(path); + uint32_t segment_index = 0; /* Iterate through each segment in the path array */ - for (idx = 0; idx < num_segments; idx++) { - /* Get the segment at the current index */ - segment_val = zend_hash_index_find(path, idx); + ZEND_HASH_FOREACH_VAL(path, segment_val) { + segment_index++; - if (segment_val == NULL) { - /* Missing segment in array */ - return NULL; - } + /* Dereference segment if it's a reference */ + ZVAL_DEREF(segment_val); /* Segment must be a string or int */ if (Z_TYPE_P(segment_val) == IS_STRING) { @@ -6945,22 +6939,27 @@ static zval* array_get_nested(HashTable *ht, HashTable *path) return NULL; } - /* If this is the last segment, return the result */ - if (idx == num_segments - 1) { - return current; + /* If segment not found, return NULL */ + if (current == NULL) { + return NULL; } - /* Check if the segment exists and is an array for next iteration */ - if (current == NULL || Z_TYPE_P(current) != IS_ARRAY) { + /* Dereference if it's a reference */ + ZVAL_DEREF(current); + + /* If current is not an array and we're not at the last segment, + * we can't continue traversing the path */ + if (Z_TYPE_P(current) != IS_ARRAY && segment_index < num_segments) { return NULL; } - /* Move to the next level */ - current_ht = Z_ARRVAL_P(current); - } + /* Update current_ht for next iteration if it's an array */ + if (Z_TYPE_P(current) == IS_ARRAY) { + current_ht = Z_ARRVAL_P(current); + } + } ZEND_HASH_FOREACH_END(); - /* Empty path array */ - return NULL; + return current; } /* }}} */ @@ -6970,7 +6969,6 @@ PHP_FUNCTION(array_get) zval *array; zval *path; zval *default_value = NULL; - zval *result; ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_ARRAY(array) @@ -6979,7 +6977,7 @@ PHP_FUNCTION(array_get) Z_PARAM_ZVAL(default_value) ZEND_PARSE_PARAMETERS_END(); - result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); + zval *result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); if (result != NULL) { RETURN_COPY_DEREF(result); @@ -6987,7 +6985,7 @@ PHP_FUNCTION(array_get) /* Path not found, return default value */ if (default_value != NULL) { - RETURN_COPY_DEREF(default_value); + RETURN_COPY(default_value); } } /* }}} */ @@ -6997,14 +6995,13 @@ PHP_FUNCTION(array_has) { zval *array; zval *path; - zval *result; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_ARRAY(array) Z_PARAM_ARRAY(path) ZEND_PARSE_PARAMETERS_END(); - result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); + zval *result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); RETURN_BOOL(result != NULL); } diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt index 44eb391f0282..a729836e201f 100644 --- a/ext/standard/tests/array/array_get.phpt +++ b/ext/standard/tests/array/array_get.phpt @@ -48,6 +48,17 @@ var_dump($result_with_ref_default); $default_value = 'changed'; var_dump($result_with_ref_default); // Should still be 'default' (not affected by reference change) +// Test with reference to an array in the path +$array2 = ['world']; +$array_with_ref = ['hello' => &$array2]; +var_dump(array_get($array_with_ref, ['hello', 0])); + +// Test with path segment that is a reference +$key1 = 'products'; +$key2 = 'desk'; +$path_with_refs = [&$key1, &$key2, 'price']; +var_dump(array_get($array, $path_with_refs)); + echo "Done"; ?> --EXPECT-- @@ -65,4 +76,6 @@ string(8) "original" string(8) "original" string(7) "default" string(7) "default" +string(5) "world" +int(100) Done diff --git a/ext/standard/tests/array/array_has.phpt b/ext/standard/tests/array/array_has.phpt index 197a2ea277fa..400bdc7730c2 100644 --- a/ext/standard/tests/array/array_has.phpt +++ b/ext/standard/tests/array/array_has.phpt @@ -34,6 +34,17 @@ var_dump(array_has($withNull, ['key'])); // Test with invalid segment type in array path var_dump(array_has($array, ['product', new stdClass()])); +// Test with reference to an array in the path +$array2 = ['world']; +$array_with_ref = ['hello' => &$array2]; +var_dump(array_has($array_with_ref, ['hello', 0])); + +// Test with path segment that is a reference +$key1 = 'product'; +$key2 = 'name'; +$path_with_refs = [&$key1, &$key2]; +var_dump(array_has($array, $path_with_refs)); + echo "Done"; ?> --EXPECT-- @@ -48,4 +59,6 @@ bool(true) bool(false) bool(true) bool(false) +bool(true) +bool(true) Done From 6b652b5c0cd7d1e807435d11eb0d13c860864e91 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Tue, 14 Apr 2026 18:21:04 +0200 Subject: [PATCH 07/10] Throw TypeError exception if segment is not int or string --- ext/standard/array.c | 11 ++++++++++- ext/standard/tests/array/array_get.phpt | 8 ++++++-- ext/standard/tests/array/array_has.phpt | 8 ++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 80638599c57d..240e95b85341 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6935,7 +6935,8 @@ static zval* array_get_nested(HashTable *ht, HashTable *path) } else if (Z_TYPE_P(segment_val) == IS_LONG) { current = zend_hash_index_find(current_ht, Z_LVAL_P(segment_val)); } else { - /* Invalid segment type */ + /* Invalid segment type - throw TypeError */ + zend_type_error("Path segment must be of type string|int, %s given", zend_zval_value_name(segment_val)); return NULL; } @@ -6979,6 +6980,10 @@ PHP_FUNCTION(array_get) zval *result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); + if (EG(exception)) { + RETURN_THROWS(); + } + if (result != NULL) { RETURN_COPY_DEREF(result); } @@ -7003,6 +7008,10 @@ PHP_FUNCTION(array_has) zval *result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); + if (EG(exception)) { + RETURN_THROWS(); + } + RETURN_BOOL(result != NULL); } /* }}} */ diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt index a729836e201f..bb52afcea884 100644 --- a/ext/standard/tests/array/array_get.phpt +++ b/ext/standard/tests/array/array_get.phpt @@ -30,7 +30,11 @@ var_dump(array_get($users, ['users', 1, 'name'])); var_dump(array_get($array, ['products', 'chair', 'price'], 75)); // Test with invalid segment type in array path -var_dump(array_get($array, ['products', new stdClass(), 'price'], 'invalid')); +try { + var_dump(array_get($array, ['products', new stdClass(), 'price'], 'invalid')); +} catch (TypeError $e) { + echo $e->getMessage() . "\n"; +} // Test with references - ensure returned value is a copy, not a reference $ref_array = ['data' => ['value' => 'original']]; @@ -71,7 +75,7 @@ NULL string(5) "Alice" string(3) "Bob" int(75) -string(7) "invalid" +Path segment must be of type string|int, stdClass given string(8) "original" string(8) "original" string(7) "default" diff --git a/ext/standard/tests/array/array_has.phpt b/ext/standard/tests/array/array_has.phpt index 400bdc7730c2..7ae1b5d7045b 100644 --- a/ext/standard/tests/array/array_has.phpt +++ b/ext/standard/tests/array/array_has.phpt @@ -32,7 +32,11 @@ $withNull = ['key' => null]; var_dump(array_has($withNull, ['key'])); // Test with invalid segment type in array path -var_dump(array_has($array, ['product', new stdClass()])); +try { + var_dump(array_has($array, ['product', new stdClass()])); +} catch (TypeError $e) { + echo $e->getMessage() . "\n"; +} // Test with reference to an array in the path $array2 = ['world']; @@ -58,7 +62,7 @@ bool(true) bool(true) bool(false) bool(true) -bool(false) +Path segment must be of type string|int, stdClass given bool(true) bool(true) Done From c9e0aa2a5bf496d8d70c1b3338f66033ccd16b3c Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Tue, 14 Apr 2026 18:27:54 +0200 Subject: [PATCH 08/10] Add _path suffix to function names --- ext/standard/array.c | 4 +-- ext/standard/basic_functions.stub.php | 4 +-- .../{array_get.phpt => array_get_path.phpt} | 32 +++++++++---------- .../{array_has.phpt => array_has_path.phpt} | 30 ++++++++--------- 4 files changed, 35 insertions(+), 35 deletions(-) rename ext/standard/tests/array/{array_get.phpt => array_get_path.phpt} (64%) rename ext/standard/tests/array/{array_has.phpt => array_has_path.phpt} (58%) diff --git a/ext/standard/array.c b/ext/standard/array.c index 240e95b85341..132a7ede9a19 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6965,7 +6965,7 @@ static zval* array_get_nested(HashTable *ht, HashTable *path) /* }}} */ /* {{{ Retrieves a value from a deeply nested array using an array path */ -PHP_FUNCTION(array_get) +PHP_FUNCTION(array_get_path) { zval *array; zval *path; @@ -6996,7 +6996,7 @@ PHP_FUNCTION(array_get) /* }}} */ /* {{{ Checks whether a given item exists in an array using an array path */ -PHP_FUNCTION(array_has) +PHP_FUNCTION(array_has_path) { zval *array; zval *path; diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 966e7a717696..793382abb114 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1910,12 +1910,12 @@ function key_exists($key, array $array): bool {} /** * @compile-time-eval */ -function array_get(array $array, array $path, mixed $default = null): mixed {} +function array_get_path(array $array, array $path, mixed $default = null): mixed {} /** * @compile-time-eval */ -function array_has(array $array, array $path): bool {} +function array_has_path(array $array, array $path): bool {} /** * @compile-time-eval diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get_path.phpt similarity index 64% rename from ext/standard/tests/array/array_get.phpt rename to ext/standard/tests/array/array_get_path.phpt index bb52afcea884..bc575620c48d 100644 --- a/ext/standard/tests/array/array_get.phpt +++ b/ext/standard/tests/array/array_get_path.phpt @@ -1,37 +1,37 @@ --TEST-- -Test array_get() function +Test array_get_path() function --FILE-- ['desk' => ['price' => 100]]]; // Test nested access with array path -var_dump(array_get($array, ['products', 'desk', 'price'])); +var_dump(array_get_path($array, ['products', 'desk', 'price'])); // Test with default value when path doesn't exist -var_dump(array_get($array, ['products', 'desk', 'discount'], 5)); +var_dump(array_get_path($array, ['products', 'desk', 'discount'], 5)); // Test simple path with single level $simple = ['name' => 'John', 'age' => 30]; -var_dump(array_get($simple, ['name'])); -var_dump(array_get($simple, ['missing'], 'default')); +var_dump(array_get_path($simple, ['name'])); +var_dump(array_get_path($simple, ['missing'], 'default')); // Test single level key that doesn't exist -var_dump(array_get($array, ['missing'])); +var_dump(array_get_path($array, ['missing'])); // Test with integer key in path $users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; -var_dump(array_get($users, ['users', 0, 'name'])); -var_dump(array_get($users, ['users', 1, 'name'])); +var_dump(array_get_path($users, ['users', 0, 'name'])); +var_dump(array_get_path($users, ['users', 1, 'name'])); // Test nested with missing intermediate key -var_dump(array_get($array, ['products', 'chair', 'price'], 75)); +var_dump(array_get_path($array, ['products', 'chair', 'price'], 75)); // Test with invalid segment type in array path try { - var_dump(array_get($array, ['products', new stdClass(), 'price'], 'invalid')); + var_dump(array_get_path($array, ['products', new stdClass(), 'price'], 'invalid')); } catch (TypeError $e) { echo $e->getMessage() . "\n"; } @@ -39,7 +39,7 @@ try { // Test with references - ensure returned value is a copy, not a reference $ref_array = ['data' => ['value' => 'original']]; $ref =& $ref_array['data']['value']; -$result = array_get($ref_array, ['data', 'value']); +$result = array_get_path($ref_array, ['data', 'value']); var_dump($result); $ref = 'modified'; var_dump($result); // Should still be 'original' (not affected by reference change) @@ -47,7 +47,7 @@ var_dump($result); // Should still be 'original' (not affected by reference chan // Test with default value being a reference $default_value = 'default'; $default_ref =& $default_value; -$result_with_ref_default = array_get($ref_array, ['missing', 'key'], $default_ref); +$result_with_ref_default = array_get_path($ref_array, ['missing', 'key'], $default_ref); var_dump($result_with_ref_default); $default_value = 'changed'; var_dump($result_with_ref_default); // Should still be 'default' (not affected by reference change) @@ -55,18 +55,18 @@ var_dump($result_with_ref_default); // Should still be 'default' (not affected b // Test with reference to an array in the path $array2 = ['world']; $array_with_ref = ['hello' => &$array2]; -var_dump(array_get($array_with_ref, ['hello', 0])); +var_dump(array_get_path($array_with_ref, ['hello', 0])); // Test with path segment that is a reference $key1 = 'products'; $key2 = 'desk'; $path_with_refs = [&$key1, &$key2, 'price']; -var_dump(array_get($array, $path_with_refs)); +var_dump(array_get_path($array, $path_with_refs)); echo "Done"; ?> --EXPECT-- -*** Testing array_get() *** +*** Testing array_get_path() *** int(100) int(5) string(4) "John" diff --git a/ext/standard/tests/array/array_has.phpt b/ext/standard/tests/array/array_has_path.phpt similarity index 58% rename from ext/standard/tests/array/array_has.phpt rename to ext/standard/tests/array/array_has_path.phpt index 7ae1b5d7045b..1cea5a27aa73 100644 --- a/ext/standard/tests/array/array_has.phpt +++ b/ext/standard/tests/array/array_has_path.phpt @@ -1,39 +1,39 @@ --TEST-- -Test array_has() function +Test array_has_path() function --FILE-- ['name' => 'Desk', 'price' => 100]]; // Test nested key exists with array path -var_dump(array_has($array, ['product', 'name'])); +var_dump(array_has_path($array, ['product', 'name'])); // Test nested key doesn't exist -var_dump(array_has($array, ['product', 'color'])); +var_dump(array_has_path($array, ['product', 'color'])); // Test intermediate key doesn't exist -var_dump(array_has($array, ['category', 'name'])); +var_dump(array_has_path($array, ['category', 'name'])); // Test simple path with single level $simple = ['name' => 'John', 'age' => 30]; -var_dump(array_has($simple, ['name'])); -var_dump(array_has($simple, ['missing'])); +var_dump(array_has_path($simple, ['name'])); +var_dump(array_has_path($simple, ['missing'])); // Test with integer key in path $users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; -var_dump(array_has($users, ['users', 0, 'name'])); -var_dump(array_has($users, ['users', 1, 'name'])); -var_dump(array_has($users, ['users', 2, 'name'])); +var_dump(array_has_path($users, ['users', 0, 'name'])); +var_dump(array_has_path($users, ['users', 1, 'name'])); +var_dump(array_has_path($users, ['users', 2, 'name'])); // Test with value that is null (key exists, but value is null) $withNull = ['key' => null]; -var_dump(array_has($withNull, ['key'])); +var_dump(array_has_path($withNull, ['key'])); // Test with invalid segment type in array path try { - var_dump(array_has($array, ['product', new stdClass()])); + var_dump(array_has_path($array, ['product', new stdClass()])); } catch (TypeError $e) { echo $e->getMessage() . "\n"; } @@ -41,18 +41,18 @@ try { // Test with reference to an array in the path $array2 = ['world']; $array_with_ref = ['hello' => &$array2]; -var_dump(array_has($array_with_ref, ['hello', 0])); +var_dump(array_has_path($array_with_ref, ['hello', 0])); // Test with path segment that is a reference $key1 = 'product'; $key2 = 'name'; $path_with_refs = [&$key1, &$key2]; -var_dump(array_has($array, $path_with_refs)); +var_dump(array_has_path($array, $path_with_refs)); echo "Done"; ?> --EXPECT-- -*** Testing array_has() *** +*** Testing array_has_path() *** bool(true) bool(false) bool(false) From 63de950fd8c83a1f0d4e2b22ac480a527177c14d Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Sat, 18 Apr 2026 07:42:17 +0200 Subject: [PATCH 09/10] Update function names to array_path_get and array_path_exists --- ext/standard/array.c | 4 +-- ext/standard/basic_functions.stub.php | 4 +-- ...y_has_path.phpt => array_path_exists.phpt} | 30 ++++++++--------- ...rray_get_path.phpt => array_path_get.phpt} | 32 +++++++++---------- 4 files changed, 35 insertions(+), 35 deletions(-) rename ext/standard/tests/array/{array_has_path.phpt => array_path_exists.phpt} (56%) rename ext/standard/tests/array/{array_get_path.phpt => array_path_get.phpt} (66%) diff --git a/ext/standard/array.c b/ext/standard/array.c index 132a7ede9a19..1293c4195b91 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6965,7 +6965,7 @@ static zval* array_get_nested(HashTable *ht, HashTable *path) /* }}} */ /* {{{ Retrieves a value from a deeply nested array using an array path */ -PHP_FUNCTION(array_get_path) +PHP_FUNCTION(array_path_get) { zval *array; zval *path; @@ -6996,7 +6996,7 @@ PHP_FUNCTION(array_get_path) /* }}} */ /* {{{ Checks whether a given item exists in an array using an array path */ -PHP_FUNCTION(array_has_path) +PHP_FUNCTION(array_path_exists) { zval *array; zval *path; diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 793382abb114..307cbee4d658 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1910,12 +1910,12 @@ function key_exists($key, array $array): bool {} /** * @compile-time-eval */ -function array_get_path(array $array, array $path, mixed $default = null): mixed {} +function array_path_get(array $array, array $path, mixed $default = null): mixed {} /** * @compile-time-eval */ -function array_has_path(array $array, array $path): bool {} +function array_path_exists(array $array, array $path): bool {} /** * @compile-time-eval diff --git a/ext/standard/tests/array/array_has_path.phpt b/ext/standard/tests/array/array_path_exists.phpt similarity index 56% rename from ext/standard/tests/array/array_has_path.phpt rename to ext/standard/tests/array/array_path_exists.phpt index 1cea5a27aa73..176f2ed7dfa5 100644 --- a/ext/standard/tests/array/array_has_path.phpt +++ b/ext/standard/tests/array/array_path_exists.phpt @@ -1,39 +1,39 @@ --TEST-- -Test array_has_path() function +Test array_path_exists() function --FILE-- ['name' => 'Desk', 'price' => 100]]; // Test nested key exists with array path -var_dump(array_has_path($array, ['product', 'name'])); +var_dump(array_path_exists($array, ['product', 'name'])); // Test nested key doesn't exist -var_dump(array_has_path($array, ['product', 'color'])); +var_dump(array_path_exists($array, ['product', 'color'])); // Test intermediate key doesn't exist -var_dump(array_has_path($array, ['category', 'name'])); +var_dump(array_path_exists($array, ['category', 'name'])); // Test simple path with single level $simple = ['name' => 'John', 'age' => 30]; -var_dump(array_has_path($simple, ['name'])); -var_dump(array_has_path($simple, ['missing'])); +var_dump(array_path_exists($simple, ['name'])); +var_dump(array_path_exists($simple, ['missing'])); // Test with integer key in path $users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; -var_dump(array_has_path($users, ['users', 0, 'name'])); -var_dump(array_has_path($users, ['users', 1, 'name'])); -var_dump(array_has_path($users, ['users', 2, 'name'])); +var_dump(array_path_exists($users, ['users', 0, 'name'])); +var_dump(array_path_exists($users, ['users', 1, 'name'])); +var_dump(array_path_exists($users, ['users', 2, 'name'])); // Test with value that is null (key exists, but value is null) $withNull = ['key' => null]; -var_dump(array_has_path($withNull, ['key'])); +var_dump(array_path_exists($withNull, ['key'])); // Test with invalid segment type in array path try { - var_dump(array_has_path($array, ['product', new stdClass()])); + var_dump(array_path_exists($array, ['product', new stdClass()])); } catch (TypeError $e) { echo $e->getMessage() . "\n"; } @@ -41,18 +41,18 @@ try { // Test with reference to an array in the path $array2 = ['world']; $array_with_ref = ['hello' => &$array2]; -var_dump(array_has_path($array_with_ref, ['hello', 0])); +var_dump(array_path_exists($array_with_ref, ['hello', 0])); // Test with path segment that is a reference $key1 = 'product'; $key2 = 'name'; $path_with_refs = [&$key1, &$key2]; -var_dump(array_has_path($array, $path_with_refs)); +var_dump(array_path_exists($array, $path_with_refs)); echo "Done"; ?> --EXPECT-- -*** Testing array_has_path() *** +*** Testing array_path_exists() *** bool(true) bool(false) bool(false) diff --git a/ext/standard/tests/array/array_get_path.phpt b/ext/standard/tests/array/array_path_get.phpt similarity index 66% rename from ext/standard/tests/array/array_get_path.phpt rename to ext/standard/tests/array/array_path_get.phpt index bc575620c48d..1a7f56f0cab3 100644 --- a/ext/standard/tests/array/array_get_path.phpt +++ b/ext/standard/tests/array/array_path_get.phpt @@ -1,37 +1,37 @@ --TEST-- -Test array_get_path() function +Test array_path_get() function --FILE-- ['desk' => ['price' => 100]]]; // Test nested access with array path -var_dump(array_get_path($array, ['products', 'desk', 'price'])); +var_dump(array_path_get($array, ['products', 'desk', 'price'])); // Test with default value when path doesn't exist -var_dump(array_get_path($array, ['products', 'desk', 'discount'], 5)); +var_dump(array_path_get($array, ['products', 'desk', 'discount'], 5)); // Test simple path with single level $simple = ['name' => 'John', 'age' => 30]; -var_dump(array_get_path($simple, ['name'])); -var_dump(array_get_path($simple, ['missing'], 'default')); +var_dump(array_path_get($simple, ['name'])); +var_dump(array_path_get($simple, ['missing'], 'default')); // Test single level key that doesn't exist -var_dump(array_get_path($array, ['missing'])); +var_dump(array_path_get($array, ['missing'])); // Test with integer key in path $users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; -var_dump(array_get_path($users, ['users', 0, 'name'])); -var_dump(array_get_path($users, ['users', 1, 'name'])); +var_dump(array_path_get($users, ['users', 0, 'name'])); +var_dump(array_path_get($users, ['users', 1, 'name'])); // Test nested with missing intermediate key -var_dump(array_get_path($array, ['products', 'chair', 'price'], 75)); +var_dump(array_path_get($array, ['products', 'chair', 'price'], 75)); // Test with invalid segment type in array path try { - var_dump(array_get_path($array, ['products', new stdClass(), 'price'], 'invalid')); + var_dump(array_path_get($array, ['products', new stdClass(), 'price'], 'invalid')); } catch (TypeError $e) { echo $e->getMessage() . "\n"; } @@ -39,7 +39,7 @@ try { // Test with references - ensure returned value is a copy, not a reference $ref_array = ['data' => ['value' => 'original']]; $ref =& $ref_array['data']['value']; -$result = array_get_path($ref_array, ['data', 'value']); +$result = array_path_get($ref_array, ['data', 'value']); var_dump($result); $ref = 'modified'; var_dump($result); // Should still be 'original' (not affected by reference change) @@ -47,7 +47,7 @@ var_dump($result); // Should still be 'original' (not affected by reference chan // Test with default value being a reference $default_value = 'default'; $default_ref =& $default_value; -$result_with_ref_default = array_get_path($ref_array, ['missing', 'key'], $default_ref); +$result_with_ref_default = array_path_get($ref_array, ['missing', 'key'], $default_ref); var_dump($result_with_ref_default); $default_value = 'changed'; var_dump($result_with_ref_default); // Should still be 'default' (not affected by reference change) @@ -55,18 +55,18 @@ var_dump($result_with_ref_default); // Should still be 'default' (not affected b // Test with reference to an array in the path $array2 = ['world']; $array_with_ref = ['hello' => &$array2]; -var_dump(array_get_path($array_with_ref, ['hello', 0])); +var_dump(array_path_get($array_with_ref, ['hello', 0])); // Test with path segment that is a reference $key1 = 'products'; $key2 = 'desk'; $path_with_refs = [&$key1, &$key2, 'price']; -var_dump(array_get_path($array, $path_with_refs)); +var_dump(array_path_get($array, $path_with_refs)); echo "Done"; ?> --EXPECT-- -*** Testing array_get_path() *** +*** Testing array_path_get() *** int(100) int(5) string(4) "John" From d7da2cbc56937df4dd7c7bbf65ef86c45e0d8470 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Sat, 25 Apr 2026 21:28:48 +0200 Subject: [PATCH 10/10] Check all keys before traversing the segments --- ext/standard/array.c | 23 +++++++++++++------ ext/standard/basic_functions_arginfo.h | 18 +++++++-------- ext/standard/basic_functions_decl.h | 8 +++---- .../tests/array/array_path_exists.phpt | 9 ++++++++ ext/standard/tests/array/array_path_get.phpt | 9 ++++++++ 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 1293c4195b91..81c42295d7ce 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6922,22 +6922,31 @@ static zval* array_get_nested(HashTable *ht, HashTable *path) uint32_t num_segments = zend_hash_num_elements(path); uint32_t segment_index = 0; - /* Iterate through each segment in the path array */ + /* First pass: validate all path segments are valid types */ + ZEND_HASH_FOREACH_VAL(path, segment_val) { + /* Dereference segment if it's a reference */ + ZVAL_DEREF(segment_val); + + /* Segment must be a string or int */ + if (Z_TYPE_P(segment_val) != IS_STRING && Z_TYPE_P(segment_val) != IS_LONG) { + /* Invalid segment type - throw TypeError */ + zend_type_error("Path segment must be of type string|int, %s given", zend_zval_value_name(segment_val)); + return NULL; + } + } ZEND_HASH_FOREACH_END(); + + /* Second pass: traverse the array using the validated path */ ZEND_HASH_FOREACH_VAL(path, segment_val) { segment_index++; /* Dereference segment if it's a reference */ ZVAL_DEREF(segment_val); - /* Segment must be a string or int */ + /* Look up the segment (already validated to be string or int) */ if (Z_TYPE_P(segment_val) == IS_STRING) { current = zend_symtable_find(current_ht, Z_STR_P(segment_val)); - } else if (Z_TYPE_P(segment_val) == IS_LONG) { - current = zend_hash_index_find(current_ht, Z_LVAL_P(segment_val)); } else { - /* Invalid segment type - throw TypeError */ - zend_type_error("Path segment must be of type string|int, %s given", zend_zval_value_name(segment_val)); - return NULL; + current = zend_hash_index_find(current_ht, Z_LVAL_P(segment_val)); } /* If segment not found, return NULL */ diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index abcb55d3e2a9..9e4d627d9caf 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 484f95966ba7e9f8d0907ef4d81c61435d3ff206 + * Stub hash: c4239cc774245794a9c54810717ae78e2b8489b1 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -376,15 +376,15 @@ ZEND_END_ARG_INFO() #define arginfo_key_exists arginfo_array_key_exists -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_get, 0, 1, IS_MIXED, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_path_get, 0, 2, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) - ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_LONG|MAY_BE_NULL, "null") + ZEND_ARG_TYPE_INFO(0, path, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_has, 0, 2, _IS_BOOL, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_path_exists, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) - ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_LONG, NULL) + ZEND_ARG_TYPE_INFO(0, path, IS_ARRAY, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_chunk, 0, 2, IS_ARRAY, 0) @@ -2417,8 +2417,8 @@ ZEND_FUNCTION(array_any); ZEND_FUNCTION(array_all); ZEND_FUNCTION(array_map); ZEND_FUNCTION(array_key_exists); -ZEND_FUNCTION(array_get); -ZEND_FUNCTION(array_has); +ZEND_FUNCTION(array_path_get); +ZEND_FUNCTION(array_path_exists); ZEND_FUNCTION(array_chunk); ZEND_FUNCTION(array_combine); ZEND_FUNCTION(array_is_list); @@ -3022,8 +3022,8 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(array_map, arginfo_array_map) ZEND_RAW_FENTRY("array_key_exists", zif_array_key_exists, arginfo_array_key_exists, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("key_exists", zif_array_key_exists, arginfo_key_exists, 0, NULL, NULL) - ZEND_RAW_FENTRY("array_get", zif_array_get, arginfo_array_get, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) - ZEND_RAW_FENTRY("array_has", zif_array_has, arginfo_array_has, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("array_path_get", zif_array_path_get, arginfo_array_path_get, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("array_path_exists", zif_array_path_exists, arginfo_array_path_exists, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("array_chunk", zif_array_chunk, arginfo_array_chunk, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("array_combine", zif_array_combine, arginfo_array_combine, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("array_is_list", zif_array_is_list, arginfo_array_is_list, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index 9592affc36f1..5977f7210652 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 484f95966ba7e9f8d0907ef4d81c61435d3ff206 */ + * Stub hash: c4239cc774245794a9c54810717ae78e2b8489b1 */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_484f95966ba7e9f8d0907ef4d81c61435d3ff206_H -#define ZEND_BASIC_FUNCTIONS_DECL_484f95966ba7e9f8d0907ef4d81c61435d3ff206_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_c4239cc774245794a9c54810717ae78e2b8489b1_H +#define ZEND_BASIC_FUNCTIONS_DECL_c4239cc774245794a9c54810717ae78e2b8489b1_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_484f95966ba7e9f8d0907ef4d81c61435d3ff206_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_c4239cc774245794a9c54810717ae78e2b8489b1_H */ diff --git a/ext/standard/tests/array/array_path_exists.phpt b/ext/standard/tests/array/array_path_exists.phpt index 176f2ed7dfa5..368f0efb5a05 100644 --- a/ext/standard/tests/array/array_path_exists.phpt +++ b/ext/standard/tests/array/array_path_exists.phpt @@ -38,6 +38,14 @@ try { echo $e->getMessage() . "\n"; } +// Test with invalid segment type even when path doesn't exist +$empty_array = []; +try { + var_dump(array_path_exists($empty_array, ['foo', 'bar', new stdClass()])); +} catch (TypeError $e) { + echo $e->getMessage() . "\n"; +} + // Test with reference to an array in the path $array2 = ['world']; $array_with_ref = ['hello' => &$array2]; @@ -63,6 +71,7 @@ bool(true) bool(false) bool(true) Path segment must be of type string|int, stdClass given +Path segment must be of type string|int, stdClass given bool(true) bool(true) Done diff --git a/ext/standard/tests/array/array_path_get.phpt b/ext/standard/tests/array/array_path_get.phpt index 1a7f56f0cab3..9b3a44e8b61a 100644 --- a/ext/standard/tests/array/array_path_get.phpt +++ b/ext/standard/tests/array/array_path_get.phpt @@ -36,6 +36,14 @@ try { echo $e->getMessage() . "\n"; } +// Test with invalid segment type even when path doesn't exist +$empty_array = []; +try { + var_dump(array_path_get($empty_array, ['foo', 'bar', new stdClass()], 'default')); +} catch (TypeError $e) { + echo $e->getMessage() . "\n"; +} + // Test with references - ensure returned value is a copy, not a reference $ref_array = ['data' => ['value' => 'original']]; $ref =& $ref_array['data']['value']; @@ -76,6 +84,7 @@ string(5) "Alice" string(3) "Bob" int(75) Path segment must be of type string|int, stdClass given +Path segment must be of type string|int, stdClass given string(8) "original" string(8) "original" string(7) "default"