Search code examples
phparrayscallbacknested-function

Dynamically nest the execution of functions using array_reduce() and an array of callbacks


I am new to PHP and am currently messing around with array_reduce, when I see this happen:

php > echo array_reduce(array("(function($x){return $x;})"), function($p, $q) {return ($q)($p);}, "init");

Warning: Uncaught Error: Call to undefined function (function(, ){return ;})() in php shell code:1
Stack trace:
#0 [internal function]: {closure}('init', '(function(, ){r...')
#1 php shell code(1): array_reduce(Array, Object(Closure), 'init')
#2 {main}
  thrown in php shell code on line 1

php > echo (function($x){return $x;})("init");
init

Essentially, there are three parts to array_reduce() - the array, the reduce function and an initial value. Here my reduce function takes two strings func($x, $y) and calls $y with the argument $x - return ($y)($x);. So I expect that calling array_reduce(array(a1, a2, a3), ..., "init"), it will return a1(a2(a3("init"))) as repeated function calls.

Hence, I also pass in an anonymous function (function($x){return $x;}) as a1, hoping it will call correctly and return "init" as the final result. Indeed when running it on its own (see the second php command), it returns "init" correctly. However when passing into array_reduce(), it does not work. Can someone help explain why, preferably in simple terms? And is it possible to achieve what I want?


Solution

  • Please be aware of the issues when using eval, as noted on the PHP documentation:

    The eval() language construct is very dangerous because it allows execution of arbitrary PHP code. Its use thus is discouraged. If you have carefully verified that there is no other option than to use this construct, pay special attention not to pass any user provided data into it without properly validating it beforehand.

    I would suggest that you only use eval for constant strings that you already have. Definitely do not allow arbitrary strings that the user has provided into eval without significant sanitisation; I would suggest that even then it is likely to be too much of a security hole.

    If you still wish to proceed with using eval, you could do something like:

    <?php
    
    $definitions = [
      '(function($x){return "a1({$x})";})',
      '(function($x){return "a2({$x})";})',
      '(function($x){return "a3({$x})";})',
    ];
    
    $functions = array_map(fn($f) => eval("return {$f};"), array_reverse($definitions));
    
    $output = array_reduce($functions, fn($p, $q) => $q($p), 'init');
    
    echo "{$output}\n";
    

    This uses:

    • array_reverse to create a reversed definitions array, as the functions would be applied from the inside out (ie, without it, the output would be a3(a2(a1(init))))
    • array_map and eval to map the array of definitions (as strings) to anonymous functions
    • arrow functions throughout as a bit of syntactic sugar to make it a bit neater