Skip to content

Commit 6a01897

Browse files
committed
Implement iterable\find($iterable, $callback, $default=null): mixed
And add checks for edge cases of Traversables
1 parent 06310e1 commit 6a01897

File tree

5 files changed

+257
-11
lines changed

5 files changed

+257
-11
lines changed

ext/standard/array.c

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5777,12 +5777,16 @@ static int php_traversable_reduce_elem(zend_object_iterator *iter, void *puser)
57775777
ZEND_ASSERT(ZEND_FCI_INITIALIZED(*fci));
57785778

57795779
zval *operand = iter->funcs->get_current_data(iter);
5780-
ZVAL_COPY_VALUE(&fci->params[0], fci->retval);
5781-
ZVAL_COPY(&fci->params[1], operand);
5780+
if (UNEXPECTED(!operand || EG(exception))) {
5781+
return ZEND_HASH_APPLY_STOP;
5782+
}
5783+
ZVAL_DEREF(operand);
5784+
ZVAL_COPY_VALUE(&reduce_data->args[0], fci->retval);
5785+
ZVAL_COPY(&reduce_data->args[1], operand);
57825786
ZVAL_NULL(fci->retval);
57835787
int result = zend_call_function(&reduce_data->fci, &reduce_data->fcc);
57845788
zval_ptr_dtor(operand);
5785-
zval_ptr_dtor(&fci->params[0]);
5789+
zval_ptr_dtor(&reduce_data->args[0]);
57865790
if (UNEXPECTED(result == FAILURE || EG(exception))) {
57875791
return ZEND_HASH_APPLY_STOP;
57885792
}
@@ -5791,12 +5795,11 @@ static int php_traversable_reduce_elem(zend_object_iterator *iter, void *puser)
57915795

57925796
static zend_always_inline void php_traversable_reduce(zval *obj, zend_fcall_info fci, zend_fcall_info_cache fci_cache, zval* return_value) /* {{{ */
57935797
{
5794-
zval args[2];
57955798
traversable_reduce_data reduce_data;
57965799
reduce_data.fci = fci;
57975800
reduce_data.fci.retval = return_value;
57985801
reduce_data.fci.param_count = 2;
5799-
reduce_data.fci.params = args;
5802+
reduce_data.fci.params = reduce_data.args;
58005803
reduce_data.fcc = fci_cache;
58015804

58025805
spl_iterator_apply(obj, php_traversable_reduce_elem, (void*)&reduce_data);
@@ -6236,7 +6239,7 @@ PHP_FUNCTION(array_combine)
62366239
static inline void php_array_until(zval *return_value, HashTable *htbl, zend_fcall_info fci, zend_fcall_info_cache fci_cache, int stop_value, int negate) /* {{{ */
62376240
{
62386241
zval args[1];
6239-
zend_bool have_callback = 0;
6242+
bool have_callback = 0;
62406243
zval *operand;
62416244
zval retval;
62426245
int result;
@@ -6254,7 +6257,7 @@ static inline void php_array_until(zval *return_value, HashTable *htbl, zend_fca
62546257

62556258
ZEND_HASH_FOREACH_VAL(htbl, operand) {
62566259
if (have_callback) {
6257-
zend_bool retval_true;
6260+
bool retval_true;
62586261
ZVAL_COPY(&args[0], operand);
62596262

62606263
/* Treat the operand like an array of size 1 */
@@ -6296,11 +6299,16 @@ static int php_traversable_func_until(zend_object_iterator *iter, void *puser) /
62966299
zval retval;
62976300
php_iterator_until_info *until_info = (php_iterator_until_info*) puser;
62986301
int result;
6299-
zend_bool stop;
6302+
bool stop;
63006303

63016304
fci = until_info->fci;
63026305
if (ZEND_FCI_INITIALIZED(fci)) {
63036306
zval *operand = iter->funcs->get_current_data(iter);
6307+
if (UNEXPECTED(!operand || EG(exception))) {
6308+
until_info->result = FAILURE;
6309+
return ZEND_HASH_APPLY_STOP;
6310+
}
6311+
ZVAL_DEREF(operand);
63046312
fci.retval = &retval;
63056313
fci.param_count = 1;
63066314
/* Use the operand like an array of size 1 */
@@ -6317,7 +6325,13 @@ static int php_traversable_func_until(zend_object_iterator *iter, void *puser) /
63176325
stop = zend_is_true(&retval) == until_info->stop_value;
63186326
zval_ptr_dtor(&retval);
63196327
} else {
6320-
stop = zend_is_true(iter->funcs->get_current_data(iter)) == until_info->stop_value;
6328+
zval *operand = iter->funcs->get_current_data(iter);
6329+
if (UNEXPECTED(!operand || EG(exception))) {
6330+
return ZEND_HASH_APPLY_STOP;
6331+
}
6332+
ZVAL_DEREF(operand);
6333+
stop = zend_is_true(operand) == until_info->stop_value;
6334+
zval_ptr_dtor(operand);
63216335
}
63226336
if (stop) {
63236337
until_info->found = 1;
@@ -6390,7 +6404,7 @@ PHP_FUNCTION(reduce)
63906404
{
63916405
zval *input;
63926406
zend_fcall_info fci;
6393-
zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
6407+
zend_fcall_info_cache fci_cache;
63946408
zval *initial = NULL;
63956409

63966410
ZEND_PARSE_PARAMETERS_START(2, 3)
@@ -6419,3 +6433,132 @@ PHP_FUNCTION(reduce)
64196433
}
64206434
}
64216435
/* }}} */
6436+
6437+
static zend_always_inline void php_array_find(HashTable *htbl, zend_fcall_info fci, zend_fcall_info_cache fci_cache, zval* return_value, zval *default_value) /* {{{ */
6438+
{
6439+
zval retval;
6440+
zval *operand;
6441+
6442+
if (zend_hash_num_elements(htbl) > 0) {
6443+
fci.retval = &retval;
6444+
fci.param_count = 1;
6445+
6446+
ZEND_HASH_FOREACH_VAL(htbl, operand) {
6447+
fci.params = operand;
6448+
Z_TRY_ADDREF_P(operand);
6449+
6450+
if (zend_call_function(&fci, &fci_cache) == SUCCESS) {
6451+
bool found = zend_is_true(&retval);
6452+
if (found) {
6453+
ZVAL_COPY_VALUE(return_value, operand);
6454+
return;
6455+
} else {
6456+
zval_ptr_dtor(operand);
6457+
}
6458+
} else {
6459+
zval_ptr_dtor(operand);
6460+
return;
6461+
}
6462+
} ZEND_HASH_FOREACH_END();
6463+
}
6464+
6465+
if (default_value) {
6466+
ZVAL_COPY(return_value, default_value);
6467+
} else {
6468+
ZVAL_NULL(return_value);
6469+
}
6470+
}
6471+
/* }}} */
6472+
6473+
typedef struct {
6474+
zend_fcall_info fci;
6475+
zend_fcall_info_cache fcc;
6476+
zval *return_value;
6477+
bool found;
6478+
} traversable_find_data;
6479+
6480+
static int php_traversable_find_elem(zend_object_iterator *iter, void *puser) /* {{{ */
6481+
{
6482+
zval retval;
6483+
traversable_find_data *find_data = puser;
6484+
zend_fcall_info *fci = &find_data->fci;
6485+
ZEND_ASSERT(ZEND_FCI_INITIALIZED(*fci));
6486+
6487+
zval *operand = iter->funcs->get_current_data(iter);
6488+
if (UNEXPECTED(!operand || EG(exception))) {
6489+
return ZEND_HASH_APPLY_STOP;
6490+
}
6491+
ZVAL_DEREF(operand);
6492+
// Treat this as a 1-element array.
6493+
fci->params = operand;
6494+
fci->retval = &retval;
6495+
Z_TRY_ADDREF_P(operand);
6496+
int result = zend_call_function(&find_data->fci, &find_data->fcc);
6497+
if (UNEXPECTED(result == FAILURE || EG(exception))) {
6498+
return ZEND_HASH_APPLY_STOP;
6499+
}
6500+
fci->retval = &retval;
6501+
bool found = zend_is_true(&retval);
6502+
zval_ptr_dtor(&retval);
6503+
if (UNEXPECTED(EG(exception))) {
6504+
return ZEND_HASH_APPLY_STOP;
6505+
}
6506+
if (found) {
6507+
ZVAL_COPY_VALUE(find_data->return_value, operand);
6508+
find_data->found = 1;
6509+
return ZEND_HASH_APPLY_STOP;
6510+
}
6511+
zval_ptr_dtor(operand);
6512+
return ZEND_HASH_APPLY_KEEP;
6513+
}
6514+
6515+
static zend_always_inline void php_traversable_find(zval *obj, zend_fcall_info fci, zend_fcall_info_cache fci_cache, zval* return_value, zval *default_value) /* {{{ */
6516+
{
6517+
traversable_find_data find_data;
6518+
find_data.fci = fci;
6519+
find_data.fci.param_count = 1;
6520+
find_data.fcc = fci_cache;
6521+
find_data.return_value = return_value;
6522+
find_data.found = 0;
6523+
6524+
if (spl_iterator_apply(obj, php_traversable_find_elem, (void*)&find_data) != SUCCESS || EG(exception)) {
6525+
return;
6526+
}
6527+
if (find_data.found) {
6528+
return;
6529+
}
6530+
if (default_value) {
6531+
ZVAL_COPY(return_value, default_value);
6532+
} else {
6533+
ZVAL_NULL(return_value);
6534+
}
6535+
}
6536+
/* }}} */
6537+
6538+
/* {{{ Finds the first value or returns the default */
6539+
PHP_FUNCTION(find)
6540+
{
6541+
zval *input;
6542+
zend_fcall_info fci;
6543+
zend_fcall_info_cache fci_cache;
6544+
zval *default_value = NULL;
6545+
6546+
ZEND_PARSE_PARAMETERS_START(2, 3)
6547+
Z_PARAM_ITERABLE(input)
6548+
Z_PARAM_FUNC(fci, fci_cache)
6549+
Z_PARAM_OPTIONAL
6550+
Z_PARAM_ZVAL(default_value)
6551+
ZEND_PARSE_PARAMETERS_END();
6552+
6553+
switch (Z_TYPE_P(input)) {
6554+
case IS_ARRAY:
6555+
php_array_find(Z_ARRVAL_P(input), fci, fci_cache, return_value, default_value);
6556+
return;
6557+
case IS_OBJECT: {
6558+
php_traversable_find(input, fci, fci_cache, return_value, default_value);
6559+
return;
6560+
}
6561+
EMPTY_SWITCH_DEFAULT_CASE();
6562+
}
6563+
}
6564+
/* }}} */

ext/standard/basic_functions.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,4 +1524,6 @@ function none(iterable $iterable, ?callable $callback = null): bool {}
15241524

15251525
function reduce(iterable $iterable, callable $callback, mixed $initial = null): mixed {}
15261526

1527+
function find(iterable $iterable, callable $callback, mixed $default = null): mixed {}
1528+
15271529
}

ext/standard/basic_functions_arginfo.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: abc9b998fe34151e0bd8b5dbbea97ada44484a08 */
2+
* Stub hash: f57589852a699be9edd08f434073007b9fef4e34 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0)
55
ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0)
@@ -2233,6 +2233,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iterable_reduce, 0, 2, IS_MIXED,
22332233
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, initial, IS_MIXED, 0, "null")
22342234
ZEND_END_ARG_INFO()
22352235

2236+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_iterable_find, 0, 2, IS_MIXED, 0)
2237+
ZEND_ARG_TYPE_INFO(0, iterable, IS_ITERABLE, 0)
2238+
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
2239+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null")
2240+
ZEND_END_ARG_INFO()
2241+
22362242

22372243
ZEND_FUNCTION(set_time_limit);
22382244
ZEND_FUNCTION(header_register_callback);
@@ -2858,6 +2864,7 @@ ZEND_FUNCTION(any);
28582864
ZEND_FUNCTION(all);
28592865
ZEND_FUNCTION(none);
28602866
ZEND_FUNCTION(reduce);
2867+
ZEND_FUNCTION(find);
28612868

28622869

28632870
static const zend_function_entry ext_functions[] = {
@@ -3515,6 +3522,7 @@ static const zend_function_entry ext_functions[] = {
35153522
ZEND_NS_FE("iterable", all, arginfo_iterable_all)
35163523
ZEND_NS_FE("iterable", none, arginfo_iterable_none)
35173524
ZEND_NS_FE("iterable", reduce, arginfo_iterable_reduce)
3525+
ZEND_NS_FE("iterable", find, arginfo_iterable_find)
35183526
ZEND_FE_END
35193527
};
35203528

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Test find() function
3+
--FILE--
4+
<?php
5+
6+
use function iterable\find;
7+
8+
/*
9+
Prototype: mixed iterable\find(array $array, callable($item): bool $callback, mixed $default = null);
10+
Description: Iterate over iterable and either return the first element matching $callback or the default.
11+
*/
12+
13+
function dump_find(...$args) {
14+
try {
15+
$result = find(...$args);
16+
echo "Result: ";
17+
var_dump($result);
18+
} catch (Error $e) {
19+
printf("Caught %s: %s\n", $e::class, $e->getMessage());
20+
}
21+
}
22+
23+
// The result of strtolower/strtoupper is locale-dependent, meaning that it cannot be converted to a constant by opcache.
24+
dump_find([]);
25+
dump_find([], function ($value) { return false; }, strtoupper('default'));
26+
dump_find(explode(' ', strtolower('ABCDEF GHIJK LMNOP QRSTUV')), function (string $item): bool {
27+
var_dump($item); return false;
28+
}, strtolower('DEFAULT'));
29+
30+
dump_find(explode(' ', strtolower('ABCDEF GHIJK')), function (string $item): bool {
31+
var_dump($item); return $item === 'ghijk';
32+
}, strtolower('DEFAULT'));
33+
34+
?>
35+
--EXPECT--
36+
Caught ArgumentCountError: iterable\find() expects at least 2 arguments, 1 given
37+
Result: string(7) "DEFAULT"
38+
string(6) "abcdef"
39+
string(5) "ghijk"
40+
string(5) "lmnop"
41+
string(6) "qrstuv"
42+
Result: string(7) "default"
43+
string(6) "abcdef"
44+
string(5) "ghijk"
45+
Result: string(5) "ghijk"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
Test find() function
3+
--FILE--
4+
<?php
5+
6+
use function iterable\find;
7+
8+
/*
9+
Prototype: mixed iterable\find(array $array, callable($item): bool $callback, mixed $default = null);
10+
Description: Iterate over iterable and either return the first element matching $callback or the default.
11+
*/
12+
13+
function dump_find(...$args) {
14+
try {
15+
$result = find(...$args);
16+
echo "Result: ";
17+
var_dump($result);
18+
} catch (Error $e) {
19+
printf("Caught %s: %s\n", $e::class, $e->getMessage());
20+
}
21+
}
22+
23+
var_dump(find(range(1, 5), fn(int $x): bool => $x * $x === 9));
24+
25+
// The result of strtolower/strtoupper is locale-dependent, meaning that it cannot be converted to a constant by opcache.
26+
dump_find(new ArrayObject([]));
27+
dump_find(new ArrayObject([]), function ($value) { return false; }, strtoupper('default'));
28+
dump_find(new ArrayObject(explode(' ', strtolower('ABCDEF GHIJK LMNOP QRSTUV'))), function (string $item): bool {
29+
var_dump($item); return false;
30+
}, strtolower('DEFAULT'));
31+
32+
dump_find(new ArrayObject(explode(' ', strtolower('ABCDEF GHIJK'))), function (string $item): bool {
33+
var_dump($item); return $item === 'ghijk';
34+
}, strtolower('DEFAULT'));
35+
36+
?>
37+
--EXPECT--
38+
int(3)
39+
Caught ArgumentCountError: iterable\find() expects at least 2 arguments, 1 given
40+
Result: string(7) "DEFAULT"
41+
string(6) "abcdef"
42+
string(5) "ghijk"
43+
string(5) "lmnop"
44+
string(6) "qrstuv"
45+
Result: string(7) "default"
46+
string(6) "abcdef"
47+
string(5) "ghijk"
48+
Result: string(5) "ghijk"

0 commit comments

Comments
 (0)