why should one prefer call_user_func_array over re

2019-01-21 07:26发布

function foobar($arg, $arg2) {
    echo __FUNCTION__, " got $arg and $arg2\n";
}
foobar('one','two'); // OUTPUTS : foobar got one and two 

call_user_func_array("foobar", array("one", "two")); // // OUTPUTS : foobar got one and two 

As I can see both regular one and call_user_func_array method both outputs same, then why should one prefer it?

In which scenario regular calling method will fail but call_user_func_array will not?

Can I get any such example?

Thank you

6条回答
小情绪 Triste *
2楼-- · 2019-01-21 08:05

In which scenario regular calling method will fail but call_user_func_array will not ?

If you don't know beforehand how many arguments you're going to pass to your function, it would be advisable to use call_user_func_array(); the only alternative is a switch statement or a bunch of conditions to accomplish a predefined subset of possibilities.

Another scenario is where the function to be called is not known beforehand, e.g. array($obj, 'method'); this is also where you could use call_user_func().

$fn = array($obj, 'method');
$args = [1, 2, 3];
call_user_func_array($fn, $args);

Note that using call_user_func_* functions can't be used to call private or protected methods.

The alternative to all of this is to make your functions accept an array as its only argument:

myfn([1, 2, 3]);

However, this eliminates the possibility to type-hint each argument in your function declaration and is generally considered a code smell.

查看更多
老娘就宠你
3楼-- · 2019-01-21 08:09

call_user_func_array performs "uncurrying", which is the opposite of "currying".

The following applies to all of PHP's "callables" (named functions, closures, methods, __invoke, etc.), so for simplicity let's ignore the differences and just focus on closures.

If we want to accept multiple arguments, PHP lets us do that with 3 different APIs. The usual way is this:

$usual = function($a, $b, $c, $d) {
             return $a + $b + $c + $d;
         };
$result = $usual(10, 20, 30, 40);  // $result == 100

Another way is called curried form:

$curried = function($a) {
               return function($b) use ($a) {
                          return function($c) use ($a, $b) {
                                     return function($d) use ($a, $b, $c) {
                                                return $a + $b + $c + $d;
                                            };
                                 };
                      };
           };
$result = call_user_func(
              call_user_func(
                  call_user_func(
                      $curried(10),
                      20),
                  30),
              40);  // $result == 100

The advantage is that all curried functions can be called in the same way: give them one argument.

If more arguments are required, more curried functions are returned, which 'remember' the previous arguments. This allows us to pass in some arguments now and the rest later.

There are some problems with this:

  • Clearly it's very tedious to write and call functions in this way.
  • If we provide curried functions, they'll be awkward whenever their 'memory' ability isn't needed.
  • If we rely on the 'memory' ability of curried functions, we'll be disappointed when other people's code doesn't provide it.

We can fix all of these issues by using a conversion function (disclaimer: that's my blog). This lets us write and call our functions in the usual way, but gives them the same 'memory' ability as if they were curried:

$curried = curry(function($a, $b, $c, $d) {
                     return $a + $b + $c + $d;
                 });
$result1 = $curried(10, 20, 30, 40);  // $result1 = 100
$result2 = call_user_func($curried(10, 20), 30, 40); // $result2 = 100

The third way is called uncurried and takes all of its arguments in one:

$uncurried = function($args) {
                 return $args[0] + $args[1] + $args[2] + $args[3];
             };
$result = $uncurried([10, 20, 30, 40]);  // $result == 100

Just like with curried functions, uncurried functions can all be called with one argument, although this time it's an array. We still face the same compatibility problems as curried functions: if we choose to use uncurried functions, we can't rely on everyone else choosing the same. Hence we also need a conversion function for uncurrying. That's what call_user_func_array does:

$uncurried = function($args) use ($usual) {
                 return call_user_func_array($usual, $args);
             };
$result1 = $usual(10, 20, 30, 40);  // $result1 = 100
$result2 = $uncurried([10, 20, 30, 40]); // $result2 = 100

Interestingly, we can get rid of that extra function($args) wrapper (a process known as "eta-reduction") by currying call_user_func_array:

$uncurried = curry('call_user_func_array', $usual);

$result = $uncurried([10, 20, 30, 40]); // $result == 100

Unfortunately call_user_func_array isn't as smart as curry; it won't automatically convert between the two. We can write our own uncurry function which has that ability:

function uncurry($f)
{
    return function($args) use ($f) {
               return call_user_func_array(
                          $f,
                          (count(func_get_args()) > 1)? func_get_args()
                                                      : $args);
           };
}

$uncurried = uncurry($usual);
$result1 = $uncurried(10, 20, 30, 40); // $result1 == 100
$result2 = $uncurried([10, 20, 30, 40]); // $result2 == 100

These conversion functions show that PHP's "usual" way of defining functions is actually redundant: if we replaced PHP's "usual" functions with 'smart' curried or uncurried ones, lots of code would carry on working. If we did that, it's better to curry everything and selectively uncurry as needed, since that's easier than going the other way around.

Unfortunately, some things which expect a variable number of arguments using func_get_args would break, as well as functions with default argument values.

Interestingly, default values are just a special form of currying. We could mostly do without them if we put those arguments first instead of last, and provided a bunch of alternative definitions which curry in the defaults. For example:

$defaults = function($a, $b, $c = 30, $d = 40) {
                return $a + $b + $c + $d;
            };
$def1 = $defaults(10, 20, 30, 40);  // $def1 == 100
$def2 = $defaults(10, 20, 30);      // $def2 == 100
$def3 = $defaults(10, 20);          // $def3 == 100

$curried = function($d, $c, $a, $b) {
               return $a + $b + $c + $d;
           };
$curriedD  = $curried(40);
$curriedDC = $curriedD(30);

$cur1 = $curried(10, 20, 30, 40);  // $cur1 == 100
$cur2 = $curriedD(10, 20, 30);     // $cur2 == 100
$cur3 = $curriedDC(10, 20);        // $cur3 == 100
查看更多
不美不萌又怎样
4楼-- · 2019-01-21 08:11

You should prefer calling the function as you'd do regularly. Use call_user_func_array with dynamic arguments. For example:

function func(arg1, arg2, arg3) {
  return "$arg1, $arg2, $arg3";
}

func(1, 2, 3); //=> "1, 2, 3"

$args = range(5,7); // dynamic arguments
call_user_func_array('func', $args); //=> "5, 6, 7"
查看更多
The star\"
5楼-- · 2019-01-21 08:11

As of php 5.6, to pass an array instead of an argument list to a function simply precede the array with an ellipsis (this is called "argument unpacking").

function foo($var1, $var2, $var3) {
   echo $var1 + $var2 + var3;
}

$array = [1,2,3];

foo(...$array);  // 6
// same as call_user_func_array('foo',$array);

The difference between call_user_func_array() and variable functions as of php 5.6 is that variable functions do not allow you to call a static method:

$params = [1,2,3,4,5];

function test_function() {
  echo implode('+',func_get_args()) .'='. array_sum(func_get_args())."\r\n";
}    

// Normal function as callback
$callback_function = 'test_function';
call_user_func_array($callback_function,$params); // 1+2+3+4+5=15
$callback_function(...$params); // 1+2+3+4+5=15

class TestClass
{
  static function testStaticMethod() {
    echo implode('+',func_get_args()) .'='. array_sum(func_get_args())."\r\n";
  }

  public function testMethod() {
    echo implode('+',func_get_args()) .'='. array_sum(func_get_args())."\r\n";
  }
}

// Class method as callback
$obj = new TestClass;
$callback_function = [$obj,'testMethod'];
call_user_func_array($callback_function,$params); // 1+2+3+4+5=15
$callback_function(...$params); // 1+2+3+4+5=15

// Static method callback
$callback_function = 'TestClass::testStaticMethod';
call_user_func_array($callback_function,$params); // 1+2+3+4+5=15
$callback_function(...$params); // Fatal error: undefined function

Php 7 adds the ability to call static methods via a variable function, so as of php 7 this difference no longer exists. In conclusion, call_user_func_array() gives your code greater compatibility.

查看更多
Animai°情兽
6楼-- · 2019-01-21 08:13
<?php

class Demo {

    public function function1() {
        echo 'in function 1';
    }
}

$obj = new Demo();

$function_list = get_class_methods('Demo');

print_r($function_list);  //Array ( [0] => function1 )

call_user_func_array(array($obj, $function_list[0]), array()); 

// Output => in function 1

?>
查看更多
SAY GOODBYE
7楼-- · 2019-01-21 08:21
  1. You have an array with the arguments for your function which is of indeterminate length.

    $args = someFuncWhichReturnsTheArgs();
    
    foobar( /* put these $args here, you do not know how many there are */ );
    

    The alternative would be:

    switch (count($args)) {
        case 1:
            foobar($args[0]);
            break;
        case 2:
            foobar($args[0], $args[1]);
            break;
        ...
    }
    

    Which is not a solution.

The use case for this may be rare, but when you come across it you need it.

查看更多
登录 后发表回答