How to use PHP function or method arguments in any order?

4.1k views Asked by At

Lets say I define class with method like this:

class Test {
    public function doStuff($a, $b, $c) {
    // -- Do stuff --
    }
}

Is it possible to use this method but with arguments in different order, like this:

$test = new Test();
$test->doStuff($b, $c, $a);

They would have the same names, but just different order.

I see Symfony2 can do it with its dispatcher, you can use arguments in any order you want. Link: Symfony2 controller can do it

The question is, how to make this work? How can Symfony2 invoke appropriate action controller, that can then accept arguments in any order you like?

Edit: I cant use arrays, and I do know that php does not use named arguments. But somehow Symfony2 manage to do it.

5

There are 5 answers

4
drew010 On BEST ANSWER

I think you are misunderstanding what Symfony is saying. You can't pass the arguments to the controller action in any order, it has to be in a specific order. What they are doing that is dynamic, however, is figuring out what order your routing parameters are in inside the function definition.

For example we define a route:

pattern:      /hello/{first_name}/{last_name}
defaults:     { _controller: AcmeHelloBundle:Hello:index, color: green }

In the route, the parameters are named first_name, last_name, and color.

What they are saying, is that it doesn't matter what order you use for the parameters in the action.

Each of the following are equivalent:

public function indexAction($first_name, $last_name, $color) {...}
public function indexAction($color, $last_name, $first_name) {...}
public function indexAction($last_name, $color, $first_name) {...}

Since your arguments are named the same as the parameters in the route, Symfony figures out what the correct order of the arguments is based on your definition.

If you were to call the following action manually:

public function indexAction($first_name, $last_name, $color)

Then the arguments still must be passed in as $first_name, $last_name, $color and not in any other order. Using a different order would just associate the wrong values with the arguments. Symfony just doesn't care what order you define your function in since it determines the order because your routing parameters must be named the same thing as your method arguments.

Hopefully that clears up the confusion.

0
d0001 On

I have a feeling they use reflection to inspect the declaration of the contoller action function; then they make the function call with the arguments in the correct order.

Take a look at http://www.php.net/manual/en/class.reflectionparameter.php. It has a getName and getPosition.

0
Samuel On

No, but you could have overloaded methods with different orders. Or you could try to do it with reflection or intelligent guessing about parameters. I doubt you could come up with an elegant solution that would work for all functions.

2
Sablefoste On

Why don't you add a fourth variable $d, which defines the incoming order to the function.

class Test {
    public function doStuff($d, $a, $b, $c) {
      $v=explode(',', $d);
      foreach($v as $key => $value){
        switch ($value){
           case 'a':
             $aa = $v[a];
             break;
           case 'b':
             $bb = $v[b];
             break;
           case 'a':
             $cc = $v[c];
             break;
          }
         echo "a is $aa, b is $bb, c is $cc"; //output  
      }

// -- Do stuff --
}
}
$d= "b,c,a"; // the order of the variables
$test = new Test();
$test->doStuff($d, $b, $c, $a);
0
Zecc On

Sable Foste's idea inspired me.

You can use another parameter to specify the order and then use variable variables:

function test($order, $_a, $_b, $_c){
  $order = explode(';', $order);
  ${$order[0]} = $_a;
  ${$order[1]} = $_b;
  ${$order[2]} = $_c;

  echo "a = $a; b = $b; c = $c<br>";
}


test('a;b;c', 'a', 'b', 'c');
test('b;a;c', 'b', 'a', 'c');

But seriously, why can't you use an array? It's the best way.


Update: I wrote this. I must have been really bored.

Now I feel dirty.

class FuncCaller{

    var $func;
    var $order_wanted;
    var $num_args_wanted;

    function FuncCaller( $func, $order ){
        $this->func = $func;
        if( is_set($order) ){
            // First version: receives string declaring parameters
            // We flip the order_wanted array so it maps name => index
            $this->order_wanted = array_flip( explode(';', $order) );  
            $this->num_args_wanted = count($this->order_wanted);
        } else {
            // Second version: we can do better, using reflection
            $func_reflection = new ReflectionFunction($this->func);
            $params = $func_reflection->getParameters();

            $this->num_args_wanted = func_reflection->getNumberOfParameters();
            $this->order_wanted = [];

            foreach( $params as $idx => $param_reflection ){
                $this->order_wanted[ $param_reflection->getName() ] = $idx;
            }
        }
    }


    function call(){
        if( func_num_args() <= 1 ){
            // Call without arguments
            return $this->func();
        }
        else if( func_num_args() == $this->num_args_wanted ){
            // order argument not present. Assume order is same as func signature
            $args = func_get_args();
            return call_user_func_array( $this->func, $args );
        }
        else {
            // @TODO: verify correct arguments were given
            $args_given = func_get_args();
            $order_given = explode( ';', array_shift($args_given) );
            $order_given = array_flip( $order_given );  // Map name to index
            $args_for_call = array();
            foreach( $this->order_wanted as $param_name => $idx ){
                $idx_given = $order_given[$param_name];
                $val = $args_given[ $idx_given ];
                $args_for_call[$idx] = $val;
            }
            return call_user_func_array( $this->func, $args_for_call );
        }
    }

    // This should allow calling the FuncCaller object as a function,        
    // but it wasn't working for me for some reason
    function __invoke(){
        $args = func_get_args();
        return call_user_func( $this->call, $args );
    }
}


// Let's create a function for testing:
function test( $first, $second, $third ){
    $first  = var_export($first , TRUE);
    $second = var_export($second, TRUE);
    $third  = var_export($third , TRUE);
    echo "Called test( $first, $second, $third );<br>";
}

// Now we test the first version: order of arguments is specified
$caller = new FuncCaller( test, '1st;2nd;3rd' );
$caller->call(1, 2, 3);
$caller->call('3rd;1st;2nd', 'c', 'a', 'b');
$caller->call('2nd;3rd;1st', 'Two', 'Three', 'One');
$caller->call('3rd;2nd;1st', 'Go!', 'Get Set...', 'Get Ready...');


echo "<br>";

// Now we test the second version: order of arguments is acquired by reflection
// Note we you won't be able to rename arguments this way, as we did above
$reflection_caller = new FuncCaller( test ); 
$reflection_caller->call(1, 2, 3);
$reflection_caller->call('third;first;second', 'c', 'a', 'b');
$reflection_caller->call('second;third;first', 'Two', 'Three', 'One');
$reflection_caller->call('third;second;first', 'Go!', 'Get Set...', 'Get Ready...');