Skip to content

Commit 1a677ec

Browse files
bugreportuserTyson Andre
authored andcommitted
Add PHP\iterable\any(...) and all(iterable $input, ?callable $cb=null)
Create iterator_every() and iterator_any() This has merge conflicts which were resolved in the subsequent commit(s). This commit was used as a basis for implementing an alternate approach to checking if all()/any() elements in an iterable satisfy a predicate. Cherry-picked from GH-3597 with modifications Combine helpers into any() and all(), modernize for php 8 Properly free the result of the callback if it was a refcounted type such as a string/array. Fix the default value of the callback. Co-Authored-By: bugreportuser Co-Authored-By: Tyson Andre <[email protected]>
1 parent b4c6d5f commit 1a677ec

File tree

7 files changed

+486
-1
lines changed

7 files changed

+486
-1
lines changed

ext/standard/array.c

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6185,3 +6185,154 @@ PHP_FUNCTION(array_combine)
61856185
} ZEND_HASH_FOREACH_END();
61866186
}
61876187
/* }}} */
6188+
6189+
static inline void php_array_until(zval *return_value, HashTable *htbl, zend_fcall_info fci, zend_fcall_info_cache fci_cache, int stop_value) /* {{{ */
6190+
{
6191+
zval args[1];
6192+
zend_bool have_callback = 0;
6193+
zval *operand;
6194+
zval retval;
6195+
int result;
6196+
6197+
if (zend_hash_num_elements(htbl) == 0) {
6198+
RETURN_BOOL(!stop_value);
6199+
}
6200+
6201+
if (ZEND_FCI_INITIALIZED(fci)) {
6202+
have_callback = 1;
6203+
fci.retval = &retval;
6204+
fci.param_count = 1;
6205+
fci.params = args;
6206+
}
6207+
6208+
ZEND_HASH_FOREACH_VAL(htbl, operand) {
6209+
if (have_callback) {
6210+
zend_bool retval_true;
6211+
ZVAL_COPY(&args[0], operand);
6212+
6213+
/* Treat the operand like an array of size 1 */
6214+
result = zend_call_function(&fci, &fci_cache);
6215+
zval_ptr_dtor(operand);
6216+
if (result == FAILURE) {
6217+
return;
6218+
}
6219+
retval_true = zend_is_true(&retval);
6220+
zval_ptr_dtor(&retval);
6221+
/* The user-provided callback rarely returns refcounted values. */
6222+
if (retval_true == stop_value) {
6223+
RETURN_BOOL(stop_value);
6224+
}
6225+
} else {
6226+
if (zend_is_true(operand) == stop_value) {
6227+
RETURN_BOOL(stop_value);
6228+
}
6229+
}
6230+
} ZEND_HASH_FOREACH_END();
6231+
6232+
RETURN_BOOL(!stop_value);
6233+
}
6234+
/* }}} */
6235+
6236+
typedef struct {
6237+
zval *obj;
6238+
int stop_value;
6239+
int result;
6240+
int found;
6241+
zend_long has_callback;
6242+
zend_fcall_info fci;
6243+
zend_fcall_info_cache fcc;
6244+
} php_iterator_until_info;
6245+
6246+
static int php_traversable_func_until(zend_object_iterator *iter, void *puser) /* {{{ */
6247+
{
6248+
zend_fcall_info fci;
6249+
zval retval;
6250+
php_iterator_until_info *until_info = (php_iterator_until_info*) puser;
6251+
int result;
6252+
zend_bool stop;
6253+
6254+
fci = until_info->fci;
6255+
if (ZEND_FCI_INITIALIZED(fci)) {
6256+
zval *operand = iter->funcs->get_current_data(iter);
6257+
fci.retval = &retval;
6258+
fci.param_count = 1;
6259+
/* Use the operand like an array of size 1 */
6260+
fci.params = operand;
6261+
fci.retval = &retval;
6262+
Z_TRY_ADDREF_P(operand);
6263+
result = zend_call_function(&fci, &until_info->fcc);
6264+
zval_ptr_dtor(operand);
6265+
if (result == FAILURE) {
6266+
until_info->result = FAILURE;
6267+
return ZEND_HASH_APPLY_STOP;
6268+
}
6269+
6270+
stop = zend_is_true(&retval) == until_info->stop_value;
6271+
zval_ptr_dtor(&retval);
6272+
} else {
6273+
stop = zend_is_true(iter->funcs->get_current_data(iter)) == until_info->stop_value;
6274+
}
6275+
if (stop) {
6276+
until_info->found = 1;
6277+
return ZEND_HASH_APPLY_STOP;
6278+
}
6279+
return ZEND_HASH_APPLY_KEEP;
6280+
}
6281+
/* }}} */
6282+
6283+
static inline void php_iterable_until(INTERNAL_FUNCTION_PARAMETERS, int stop_value) /* {{{ */
6284+
{
6285+
zval *input;
6286+
zend_fcall_info fci = empty_fcall_info;
6287+
zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
6288+
6289+
ZEND_PARSE_PARAMETERS_START(1, 2)
6290+
Z_PARAM_ZVAL(input)
6291+
Z_PARAM_OPTIONAL
6292+
Z_PARAM_FUNC_OR_NULL(fci, fci_cache)
6293+
ZEND_PARSE_PARAMETERS_END();
6294+
6295+
switch (Z_TYPE_P(input)) {
6296+
case IS_ARRAY:
6297+
php_array_until(return_value, Z_ARRVAL_P(input), fci, fci_cache, stop_value);
6298+
return;
6299+
case IS_OBJECT:
6300+
if (instanceof_function(Z_OBJCE_P(input), zend_ce_traversable)) {
6301+
php_iterator_until_info until_info;
6302+
6303+
until_info.obj = input;
6304+
until_info.fci = fci;
6305+
until_info.fcc = fci_cache;
6306+
6307+
until_info.stop_value = stop_value;
6308+
until_info.result = SUCCESS;
6309+
until_info.found = 0;
6310+
6311+
if (spl_iterator_apply(until_info.obj, php_traversable_func_until, (void*)&until_info) == SUCCESS && until_info.result == SUCCESS) {
6312+
RETURN_BOOL(!(until_info.found ^ stop_value));
6313+
}
6314+
return;
6315+
}
6316+
/* fallthrough */
6317+
default:
6318+
zend_argument_type_error(1, "must be of type iterable, %s given", zend_zval_type_name(input));
6319+
RETURN_THROWS();
6320+
break;
6321+
}
6322+
}
6323+
6324+
/* {{{ proto bool all(array input, ?callable predicate = null)
6325+
Determines whether the predicate holds for all elements in the array */
6326+
PHP_FUNCTION(all)
6327+
{
6328+
php_iterable_until(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
6329+
}
6330+
/* }}} */
6331+
6332+
/* {{{ proto bool any(array input, ?callable predicate = null)
6333+
Determines whether the predicate holds for at least one element in the array */
6334+
PHP_FUNCTION(any)
6335+
{
6336+
php_iterable_until(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
6337+
}
6338+
/* }}} */

ext/standard/basic_functions.stub.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
/** @generate-function-entries */
44

5+
namespace {
6+
57
/* main/main.c */
68

79
function set_time_limit(int $seconds): bool {}
@@ -1502,3 +1504,12 @@ function sapi_windows_set_ctrl_handler(?callable $handler, bool $add = true): bo
15021504

15031505
function sapi_windows_generate_ctrl_event(int $event, int $pid = 0): bool {}
15041506
#endif
1507+
} // end global namespace
1508+
1509+
namespace PHP\iterable {
1510+
1511+
function any(iterable $iterable, ?callable $callback = null): bool {}
1512+
1513+
function all(iterable $iterable, ?callable $callback = null): bool {}
1514+
1515+
}

ext/standard/basic_functions_arginfo.h

Lines changed: 12 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: 7540039937587f05584660bc1a1a8a80aa5ccbd1 */
2+
* Stub hash: ae85975ce0d56215175366571e89efa400efa44e */
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)
@@ -2215,6 +2215,13 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_sapi_windows_generate_ctrl_event
22152215
ZEND_END_ARG_INFO()
22162216
#endif
22172217

2218+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_PHP_iterable_any, 0, 1, _IS_BOOL, 0)
2219+
ZEND_ARG_TYPE_INFO(0, iterable, IS_ITERABLE, 0)
2220+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, callback, IS_CALLABLE, 1, "null")
2221+
ZEND_END_ARG_INFO()
2222+
2223+
#define arginfo_PHP_iterable_all arginfo_PHP_iterable_any
2224+
22182225

22192226
ZEND_FUNCTION(set_time_limit);
22202227
ZEND_FUNCTION(header_register_callback);
@@ -2834,6 +2841,8 @@ ZEND_FUNCTION(sapi_windows_set_ctrl_handler);
28342841
#if defined(PHP_WIN32)
28352842
ZEND_FUNCTION(sapi_windows_generate_ctrl_event);
28362843
#endif
2844+
ZEND_FUNCTION(any);
2845+
ZEND_FUNCTION(all);
28372846

28382847

28392848
static const zend_function_entry ext_functions[] = {
@@ -3486,5 +3495,7 @@ static const zend_function_entry ext_functions[] = {
34863495
#if defined(PHP_WIN32)
34873496
ZEND_FE(sapi_windows_generate_ctrl_event, arginfo_sapi_windows_generate_ctrl_event)
34883497
#endif
3498+
ZEND_NS_FE("PHP\\iterable", any, arginfo_PHP_iterable_any)
3499+
ZEND_NS_FE("PHP\\iterable", all, arginfo_PHP_iterable_all)
34893500
ZEND_FE_END
34903501
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
Test all() function
3+
--FILE--
4+
<?php
5+
6+
use function PHP\iterable\all;
7+
8+
/*
9+
Prototype: bool all(array $array, mixed $callback);
10+
Description: Iterate array and stop based on return value of callback
11+
*/
12+
13+
function is_int_ex($item)
14+
{
15+
return is_int($item);
16+
}
17+
18+
echo "\n*** Testing not enough or wrong arguments ***\n";
19+
20+
function dump_all(...$args) {
21+
try {
22+
var_dump(all(...$args));
23+
} catch (Error $e) {
24+
printf("Caught %s: %s\n", $e::class, $e->getMessage());
25+
}
26+
}
27+
28+
dump_all();
29+
dump_all(true);
30+
dump_all([]);
31+
dump_all(true, function () {});
32+
dump_all([], true);
33+
34+
echo "\n*** Testing basic functionality ***\n";
35+
36+
dump_all([1, 2, 3], 'is_int_ex');
37+
dump_all(['hello', 1, 2, 3], 'is_int_ex');
38+
$iterations = 0;
39+
dump_all(['hello', 1, 2, 3], function($item) use (&$iterations) {
40+
++$iterations;
41+
return is_int($item);
42+
});
43+
var_dump($iterations);
44+
45+
echo "\n*** Testing edge cases ***\n";
46+
47+
dump_all([], 'is_int_ex');
48+
49+
echo "\nDone";
50+
?>
51+
--EXPECT--
52+
*** Testing not enough or wrong arguments ***
53+
Caught ArgumentCountError: PHP\iterable\all() expects at least 1 argument, 0 given
54+
Caught TypeError: PHP\iterable\all(): Argument #1 ($iterable) must be of type iterable, bool given
55+
bool(true)
56+
Caught TypeError: PHP\iterable\all(): Argument #1 ($iterable) must be of type iterable, bool given
57+
Caught TypeError: PHP\iterable\all(): Argument #2 ($callback) must be a valid callback, no array or string given
58+
59+
*** Testing basic functionality ***
60+
bool(true)
61+
bool(false)
62+
bool(false)
63+
int(1)
64+
65+
*** Testing edge cases ***
66+
bool(true)
67+
68+
Done
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
Test all() function
3+
--FILE--
4+
<?php
5+
6+
use function php\iterable\all;
7+
8+
/*
9+
Prototype: bool all(array $array, ?callable $callback = null, int $use_type = 0);
10+
Description: Iterate array and stop based on return value of callback
11+
*/
12+
13+
function is_int_ex($item)
14+
{
15+
return is_int($item);
16+
}
17+
18+
function dump_all(...$args) {
19+
try {
20+
var_dump(all(...$args));
21+
} catch (Error $e) {
22+
printf("Caught %s: %s\n", $e::class, $e->getMessage());
23+
}
24+
}
25+
26+
27+
echo "\n*** Testing not enough or wrong arguments ***\n";
28+
29+
dump_all(new ArrayIterator());
30+
dump_all(new ArrayIterator(), true);
31+
32+
echo "\n*** Testing basic functionality ***\n";
33+
34+
dump_all(new ArrayIterator([1, 2, 3]), 'is_int_ex');
35+
dump_all(new ArrayIterator(['hello', 1, 2, 3]), 'is_int_ex');
36+
$iterations = 0;
37+
dump_all(new ArrayIterator(['hello', 1, 2, 3]), function($item) use (&$iterations) {
38+
++$iterations;
39+
return is_int($item);
40+
});
41+
var_dump($iterations);
42+
43+
echo "\n*** Testing edge cases ***\n";
44+
45+
dump_all(new ArrayIterator(), 'is_int_ex');
46+
47+
echo "\nDone";
48+
?>
49+
--EXPECT--
50+
*** Testing not enough or wrong arguments ***
51+
bool(true)
52+
Caught TypeError: PHP\iterable\all(): Argument #2 ($callback) must be a valid callback, no array or string given
53+
54+
*** Testing basic functionality ***
55+
bool(true)
56+
bool(false)
57+
bool(false)
58+
int(1)
59+
60+
*** Testing edge cases ***
61+
bool(true)
62+
63+
Done

0 commit comments

Comments
 (0)