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
154 changes: 154 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
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.

This won't work properly with references I believe.
The function stub doesn't seem to return a reference, so this needs to be RETURN_COPY_DEREF (same issue is also present in other places).

}
}

/* 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);
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.

This is a bit of an inefficient way to go about it.


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);
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.

This is insufficient, the ZPP parsing still accepts any argument. The stubs don't enforce the argument type check in any way.

RETURN_BOOL(zend_hash_index_exists(ht, Z_LVAL_P(key)));
}
/* }}} */

/* {{{ 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_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
*/
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.

77 changes: 77 additions & 0 deletions ext/standard/tests/array/array_get.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--TEST--
Test array_get() function
--FILE--
<?php
/*
* Test functionality of array_get()
*/

echo "*** Testing array_get() ***\n";

// Basic array access
$array = ['products' => ['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
74 changes: 74 additions & 0 deletions ext/standard/tests/array/array_has.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
--TEST--
Test array_has() function
--FILE--
<?php
/*
* Test functionality of array_has()
*/

echo "*** Testing array_has() ***\n";

// Basic array
$array = ['product' => ['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
Loading