Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -6915,6 +6915,109 @@ 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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where possible, please merge the declaration and assignment. Splitting them is bad practice nowadays because the scope of the variable is unclear.

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 */
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 */
if (Z_TYPE_P(segment_val) == IS_STRING) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and references are also not handled for segment_val. You can use zend_hash_index_find_deref, or if you switch to a ZEND_HASH_FOREACH loop use something like ZVAL_DEREF.

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;
}

/* 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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where possible, please merge the declaration and assignment. Splitting them is bad practice nowadays because the scope of the variable is unclear.

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)
{
Expand Down
10 changes: 10 additions & 0 deletions ext/standard/basic_functions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,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
*/
Expand Down
17 changes: 16 additions & 1 deletion ext/standard/basic_functions_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions ext/standard/basic_functions_decl.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions ext/standard/tests/array/array_path_exists.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
--TEST--
Test array_path_exists() function
--FILE--
<?php
echo "*** Testing array_path_exists() ***\n";

// Basic array
$array = ['product' => ['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 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
bool(true)
bool(true)
Done
85 changes: 85 additions & 0 deletions ext/standard/tests/array/array_path_get.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
--TEST--
Test array_path_get() function
--FILE--
<?php
echo "*** Testing array_path_get() ***\n";

// Basic nested array access
$array = ['products' => ['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 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
string(8) "original"
string(8) "original"
string(7) "default"
string(7) "default"
string(5) "world"
int(100)
Done
Loading