Skip to content
Open
50 changes: 50 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -7371,3 +7371,53 @@ PHP_FUNCTION(array_combine)
} ZEND_HASH_FOREACH_END();
}
/* }}} */
/* {{{ Filters array elements that contain a specific substring, preserving keys */
PHP_FUNCTION(array_str_contains)
{
HashTable *haystack;
zend_string *needle;
zval *val;
zend_string *key;
zend_ulong num_key;

ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_ARRAY_HT(haystack)
Z_PARAM_STR(needle)
ZEND_PARSE_PARAMETERS_END();

array_init(return_value);

if (zend_hash_num_elements(haystack) == 0) {
return;
}

/* Empty needle matches every string element */
bool needle_is_empty = (ZSTR_LEN(needle) == 0);

ZEND_HASH_FOREACH_KEY_VAL(haystack, num_key, key, val) {
ZVAL_DEREF(val);

if (Z_TYPE_P(val) != IS_STRING) {
continue;
}

bool match = false;
if (needle_is_empty) {
match = true;
} else if (Z_STRLEN_P(val) >= ZSTR_LEN(needle)) {
if (php_memnstr(Z_STRVAL_P(val), ZSTR_VAL(needle), ZSTR_LEN(needle), Z_STRVAL_P(val) + Z_STRLEN_P(val))) {
match = true;
}
}

if (match) {
Z_TRY_ADDREF_P(val);
if (key) {
zend_hash_add_new(Z_ARRVAL_P(return_value), key, val);
} else {
zend_hash_index_add_new(Z_ARRVAL_P(return_value), num_key, val);
}
}
} ZEND_HASH_FOREACH_END();
}
/* }}} */
6 changes: 6 additions & 0 deletions ext/standard/basic_functions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,12 @@ function array_walk_recursive(array|object &$array, callable $callback, mixed $a
*/
function in_array(mixed $needle, array $haystack, bool $strict = false): bool {}

/**
* @param array $haystack
* @return array
*/
function array_str_contains(array $haystack, string $needle): array {}

/**
* @compile-time-eval
*/
Expand Down
9 changes: 8 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.

2 changes: 2 additions & 0 deletions ext/standard/php_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,6 @@ ZEND_END_MODULE_GLOBALS(array)

#define ARRAYG(v) ZEND_MODULE_GLOBALS_ACCESSOR(array, v)

PHP_FUNCTION(array_str_contains);

#endif /* PHP_ARRAY_H */
48 changes: 48 additions & 0 deletions ext/standard/tests/array/array_str_contains.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
array_str_contains() function - basic and edge cases
--FILE--
<?php

$haystack = ["apple", "banana", "cherry", "date", "elderberry"];

// 1. Basic matching
var_dump(array_str_contains($haystack, "an"));
var_dump(array_str_contains($haystack, "berry"));
var_dump(array_str_contains($haystack, "xyz"));

// 2. Preserves array keys
$assoc = ["a" => "item_one", "b" => "item_two", "c" => "other"];
var_dump(array_str_contains($assoc, "item"));

// 3. Empty needle matches everything
var_dump(array_str_contains(["foo", "bar"], ""));

// 4. Empty array returns empty array
var_dump(array_str_contains([], "test"));

?>
--EXPECT--
array(1) {
[1]=>
string(6) "banana"
}
array(1) {
[4]=>
string(10) "elderberry"
}
array(0) {
}
array(2) {
["a"]=>
string(8) "item_one"
["b"]=>
string(8) "item_two"
}
array(2) {
[0]=>
string(3) "foo"
[1]=>
string(3) "bar"
}
array(0) {
}
53 changes: 53 additions & 0 deletions ext/standard/tests/array_str_contains_benchmark.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
Benchmark array_str_contains vs userland implementations
--FILE--
<?php

$iterations = 100_000;
$haystack_start = array_merge(['apple'], array_fill(0, 50, 'banana'));
$haystack_mid = array_merge(array_fill(0, 25, 'banana'), ['apple'], array_fill(0, 25, 'banana'));
$haystack_end = array_merge(array_fill(0, 50, 'banana'), ['apple']);
$haystack_none = array_fill(0, 50, 'banana');

$cases = [
'Match at start' => $haystack_start,
'Match in middle' => $haystack_mid,
'Match at end' => $haystack_end,
'No match (all)' => $haystack_none,
];

echo "=== BENCHMARK RESULTS (" . number_format($iterations) . " iterations) ===\n\n";

foreach ($cases as $label => $array) {
echo "[$label]\n";

// 1. C implementation (array_str_contains)
$t1 = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$res1 = array_str_contains($array, 'apple');
}
$c_time = (hrtime(true) - $t1) / 1e6; // ms

// 2. Userland foreach + str_contains
$t2 = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$res2 = false;
foreach ($array as $item) {
if (is_string($item) && str_contains($item, 'apple')) {
$res2 = true;
break;
}
}
}
$userland_time = (hrtime(true) - $t2) / 1e6; // ms

$speedup = ($userland_time / $c_time);

printf(" C (array_str_contains) : %7.2f ms\n", $c_time);
printf(" Userland (foreach) : %7.2f ms\n", $userland_time);
printf(" Speedup : %7.2fx faster\n\n", $speedup);
}
?>
--EXPECTF--
=== BENCHMARK RESULTS (%s iterations) ===
%a
Loading