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

5

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.

Lunar answered 19/10, 2012 at 20:4 Comment(6)
@kalpaitch - well, symfony2 does it, so there is a way.Lunar
Take a look at the suggestions here: https://mcmap.net/q/117088/-php-function-overloadingRuler
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.Ruler
@Lunar Edit: meaning not without help, thanks to other comments for clarifyingSupple
possible duplicate of Passing named parameters to a php function through call_user_func_arrayCarmencarmena
I find $test->doStuff(array("b" => $b, "c" => $c, "a" => $a)); to work just fine and it's simple enough to implement.Ose
P
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.

Pushbike answered 19/10, 2012 at 20:16 Comment(4)
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.Ose
@Ose 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 and then ReflectionMethod on the class method to get the order of arguments.Pushbike
Ah that's right, it can be done with the Reflection classes aswell.Ose
@Ose Take a look at this question: #12977355 This answers how to instantiate class and call method with arguments in any order :)Lunar
A
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...');
Abdu answered 19/10, 2012 at 21:55 Comment(0)
M
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.

Mnemosyne answered 19/10, 2012 at 20:9 Comment(0)
A
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.

Avocet answered 20/10, 2012 at 12:56 Comment(0)
C
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);
Creditor answered 19/10, 2012 at 20:23 Comment(2)
@Zecc, yeah, sorry, I wasn't able to test the code, just a sketch. I like what you did above, however.Creditor
Thanks. You inspired me. And I had some free time to fully debug it :)Abdu

© 2022 - 2024 — McMap. All rights reserved.