6

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.

PeeHaa
  • 71,436
  • 58
  • 190
  • 262
otporan
  • 1,345
  • 2
  • 12
  • 29
  • 2
    @kalpaitch - well, symfony2 does it, so there is a way. – otporan Oct 19 '12 at 20:06
  • 1
    Take a look at the suggestions here: http://stackoverflow.com/questions/4697705/php-function-overloading – Steve Goodman Oct 19 '12 at 20:08
  • 1
    Yeah, they probably inspect the object types of each argument. This will only work if your method signatures have arguments which are all distinct data types. E.g. if you're planning on passing three strings, in any order, you're out of luck. – Steve Goodman Oct 19 '12 at 20:11
  • 1
    @otporan Edit: meaning not without help, thanks to other comments for clarifying – kalpaitch Oct 19 '12 at 20:12
  • possible duplicate of [Passing named parameters to a php function through call\_user\_func\_array](http://stackoverflow.com/questions/6610556/passing-named-parameters-to-a-php-function-through-call-user-func-array) – mario Oct 19 '12 at 20:40
  • I find `$test->doStuff(array("b" => $b, "c" => $c, "a" => $a));` to work just fine and it's simple enough to implement. – Mahn Oct 19 '12 at 21:12

5 Answers5

8

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.

drew010
  • 68,777
  • 11
  • 134
  • 162
  • So how do they figure out dynamically the order in which the arguments appear in the function definition, anyway? I don't think this can be done programmatically unless the file is opened as text and parsed. – Mahn Oct 19 '12 at 21:14
  • 3
    @Mahn They key is that the function parameters must be named exactly the same as the route parameters. Then, to get the info on the class, method, and its arguments, they probably use [ReflectionClass](http://php.net/reflectionclass) and then [ReflectionMethod](http://www.php.net/manual/en/class.reflectionmethod.php) on the class method to get the order of arguments. – drew010 Oct 19 '12 at 21:25
  • Ah that's right, it can be done with the Reflection classes aswell. – Mahn Oct 20 '12 at 01:05
  • @Mahn Take a look at this question: http://stackoverflow.com/questions/12976855/php-call-user-function-array-or-reflection-class-pass-by-reference This answers how to instantiate class and call method with arguments in any order :) – otporan Oct 20 '12 at 08:44
4

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...');
Zecc
  • 4,220
  • 19
  • 17
2

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.

Samuel
  • 16,923
  • 6
  • 62
  • 75
2

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.

d0001
  • 2,162
  • 3
  • 20
  • 46
1

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);
Sablefoste
  • 4,032
  • 3
  • 37
  • 58