diff --git a/ext/standard/array.c b/ext/standard/array.c index 7aafe6ea0a176..1626b29fcc279 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6910,6 +6910,160 @@ 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) +{ + zval *segment_val; + zval *current; + HashTable *current_ht; + uint32_t idx; + uint32_t num_segments; + + current_ht = ht; + num_segments = zend_hash_num_elements(segments); + + /* 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; + } + + /* 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 level */ + current_ht = Z_ARRVAL_P(current); + } + + /* Empty segments array */ + return NULL; +} +/* }}} */ + +/* {{{ Retrieves a value from a deeply nested array using "dot" notation */ +PHP_FUNCTION(array_get) +{ + 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(array) + 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) { + RETURN_COPY(array); + } + + 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) { + 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); + + 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) { + RETURN_COPY(result); + } + } + + /* Key not found, return default value */ + if (default_value != NULL) { + RETURN_COPY(default_value); + } +} +/* }}} */ + +/* {{{ Checks whether a given item exists in an array using "dot" notation */ +PHP_FUNCTION(array_has) +{ + zval *array; + zval *key; + zval *result; + zval segments_array; + HashTable *ht; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(array) + Z_PARAM_ZVAL(key) + ZEND_PARSE_PARAMETERS_END(); + + 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); + + result = array_get_nested(ht, Z_ARRVAL(segments_array)); + + zval_ptr_dtor(&segments_array); + RETURN_BOOL(result != NULL); + } + + /* Handle integer keys (simple lookup) */ + ZEND_ASSERT(Z_TYPE_P(key) == IS_LONG); + RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key))); +} +/* }}} */ + /* {{{ 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 1f3d5617f8dfb..dac163777c932 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1903,6 +1903,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|array|null $key = null, mixed $default = null): mixed {} + +/** + * @compile-time-eval + */ +function array_has(array $array, string|int|array $key): bool {} + /** * @compile-time-eval */ diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 991d76d91fc1c..37081b47315ca 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: 749c71a6220260eb3fb593b982a9d97821e0539b + * Stub hash: 2f12dc85580fe1fe947f2de647cfadf6e7696640 * 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_ARRAY|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|MAY_BE_ARRAY, 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); @@ -3001,6 +3014,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 fce41100fc79a..e67f76aa2a849 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: 749c71a6220260eb3fb593b982a9d97821e0539b */ + * Stub hash: 2f12dc85580fe1fe947f2de647cfadf6e7696640 */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_749c71a6220260eb3fb593b982a9d97821e0539b_H -#define ZEND_BASIC_FUNCTIONS_DECL_749c71a6220260eb3fb593b982a9d97821e0539b_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_2f12dc85580fe1fe947f2de647cfadf6e7696640_H +#define ZEND_BASIC_FUNCTIONS_DECL_2f12dc85580fe1fe947f2de647cfadf6e7696640_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_749c71a6220260eb3fb593b982a9d97821e0539b_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_2f12dc85580fe1fe947f2de647cfadf6e7696640_H */ diff --git a/ext/standard/tests/array/array_get.phpt b/ext/standard/tests/array/array_get.phpt new file mode 100644 index 0000000000000..00678ff0bd02d --- /dev/null +++ b/ext/standard/tests/array/array_get.phpt @@ -0,0 +1,77 @@ +--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)); + +// 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-- +*** 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) +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 new file mode 100644 index 0000000000000..14fbe1598b40f --- /dev/null +++ b/ext/standard/tests/array/array_has.phpt @@ -0,0 +1,74 @@ +--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')); + +// 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-- +*** 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) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +Done