diff --git a/ext/standard/array.c b/ext/standard/array.c index e175b7b8d2f4..81c42295d7ce 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6913,6 +6913,118 @@ PHP_FUNCTION(array_key_exists) } /* }}} */ +/* {{{ 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 = NULL; + HashTable *current_ht = ht; + uint32_t num_segments = zend_hash_num_elements(path); + uint32_t segment_index = 0; + + /* 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); + + /* 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 { + current = zend_hash_index_find(current_ht, Z_LVAL_P(segment_val)); + } + + /* If segment not found, return NULL */ + if (current == NULL) { + return NULL; + } + + /* 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; + } + + /* 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(); + + return current; +} +/* }}} */ + +/* {{{ Retrieves a value from a deeply nested array using an array path */ +PHP_FUNCTION(array_path_get) +{ + zval *array; + zval *path; + zval *default_value = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_ARRAY(array) + Z_PARAM_ARRAY(path) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + 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); + } + + /* Path not found, return default value */ + if (default_value != NULL) { + RETURN_COPY(default_value); + } +} +/* }}} */ + +/* {{{ Checks whether a given item exists in an array using an array path */ +PHP_FUNCTION(array_path_exists) +{ + zval *array; + zval *path; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(array) + Z_PARAM_ARRAY(path) + ZEND_PARSE_PARAMETERS_END(); + + zval *result = array_get_nested(Z_ARRVAL_P(array), Z_ARRVAL_P(path)); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_BOOL(result != NULL); +} +/* }}} */ + /* {{{ 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..307cbee4d658 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_path_get(array $array, array $path, mixed $default = null): mixed {} + +/** + * @compile-time-eval + */ +function array_path_exists(array $array, array $path): bool {} + /** * @compile-time-eval */ diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index e51a837ffa4d..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: 36b71aa7bbfe478a5e4af400b2822a77067efa2f + * 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,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_path_get, 0, 2, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) + 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_path_exists, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) + 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) 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_path_get); +ZEND_FUNCTION(array_path_exists); 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_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 b3eb25c5d988..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: 36b71aa7bbfe478a5e4af400b2822a77067efa2f */ + * Stub hash: c4239cc774245794a9c54810717ae78e2b8489b1 */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_36b71aa7bbfe478a5e4af400b2822a77067efa2f_H -#define ZEND_BASIC_FUNCTIONS_DECL_36b71aa7bbfe478a5e4af400b2822a77067efa2f_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_36b71aa7bbfe478a5e4af400b2822a77067efa2f_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 new file mode 100644 index 000000000000..368f0efb5a05 --- /dev/null +++ b/ext/standard/tests/array/array_path_exists.phpt @@ -0,0 +1,77 @@ +--TEST-- +Test array_path_exists() function +--FILE-- + ['name' => 'Desk', 'price' => 100]]; + +// Test nested key exists with array path +var_dump(array_path_exists($array, ['product', 'name'])); + +// Test nested key doesn't exist +var_dump(array_path_exists($array, ['product', 'color'])); + +// Test intermediate key doesn't exist +var_dump(array_path_exists($array, ['category', 'name'])); + +// Test simple path with single level +$simple = ['name' => 'John', 'age' => 30]; +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_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_path_exists($withNull, ['key'])); + +// Test with invalid segment type in array path +try { + var_dump(array_path_exists($array, ['product', new stdClass()])); +} catch (TypeError $e) { + 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]; +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_path_exists($array, $path_with_refs)); + +echo "Done"; +?> +--EXPECT-- +*** Testing array_path_exists() *** +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +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 new file mode 100644 index 000000000000..9b3a44e8b61a --- /dev/null +++ b/ext/standard/tests/array/array_path_get.phpt @@ -0,0 +1,94 @@ +--TEST-- +Test array_path_get() function +--FILE-- + ['desk' => ['price' => 100]]]; + +// Test nested access with array path +var_dump(array_path_get($array, ['products', 'desk', 'price'])); + +// Test with default value when path doesn't exist +var_dump(array_path_get($array, ['products', 'desk', 'discount'], 5)); + +// Test simple path with single level +$simple = ['name' => 'John', 'age' => 30]; +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_path_get($array, ['missing'])); + +// Test with integer key in path +$users = ['users' => [['name' => 'Alice'], ['name' => 'Bob']]]; +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_path_get($array, ['products', 'chair', 'price'], 75)); + +// Test with invalid segment type in array path +try { + var_dump(array_path_get($array, ['products', new stdClass(), 'price'], 'invalid')); +} catch (TypeError $e) { + 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']; +$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) + +// Test with default value being a reference +$default_value = 'default'; +$default_ref =& $default_value; +$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) + +// Test with reference to an array in the path +$array2 = ['world']; +$array_with_ref = ['hello' => &$array2]; +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_path_get($array, $path_with_refs)); + +echo "Done"; +?> +--EXPECT-- +*** Testing array_path_get() *** +int(100) +int(5) +string(4) "John" +string(7) "default" +NULL +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" +string(7) "default" +string(5) "world" +int(100) +Done