Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
108 changes: 108 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -6910,6 +6910,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);
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.

If you have a zend_string already here, then you can avoid the allocation.
In general, use zend_symtable_str_find (also below).

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

Does this handle references properly?

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

Unless this gets tailcall eliminated, you have primitive recursion here which can overflow. Best to write an explicit loop.

}
/* }}} */

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

Avoid a duplication, just return a copy with the refcount. Sadly, ZVAL_ARR sucks as it doesn't take into account the mutability in the type info; so you'd have to use Z_PARAM_ARRAY so you have a zval that you can use RETURN_COPY on.

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

RETURN_COPY

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

RETURN_COPY

return;
}
}

/* Key not found, return default value */
if (default_value != NULL) {
ZVAL_COPY(return_value, default_value);
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.

RETURN_COPY

} else {
RETVAL_NULL();
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.

Not necessary, the return value is NULL implicitly

}
}
/* }}} */

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

@ndossche ndossche Apr 4, 2026

Choose a reason for hiding this comment

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

This case is impossible by using the type int|string, use ZEND_UNREACHABLE();

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.

Not yet impossible, because Z_PARAM_ZVAL, but yes, it should be properly typed in ZPP.

}
/* }}} */

/* {{{ 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|null $key = null, mixed $default = null): mixed {}

/**
* @compile-time-eval
*/
function array_has(array $array, string|int $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.

63 changes: 63 additions & 0 deletions ext/standard/tests/array/array_get.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--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));

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
60 changes: 60 additions & 0 deletions ext/standard/tests/array/array_has.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
--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'));

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
Loading