phpvariadic-functionsphp-8named-parameters

Is there a way to use both named args and variable args in a function call?


Going through a code review and found named argument call being used to a argument that uses the spread operator for variable args (vargs). To my suprise it works fine for 1 argument, but I cannot find any information on how I would use named arguments with additional arguments being passed:

<?php

function foo(...$vargs) {
    echo implode(',', $vargs);
}

// normal usage of vargs
foo(1, 2, 3); // echo 1,2,3

// single arg fine got variable number args
foo(vargs: 4); // echo 4

// runs but puts the whole array into the 1st varg
foo(vargs: [4, 5]);

// no good all have syntax errors
foo(vargs: ...[4, 5]);
foo(vargs: 4, vargs: 5);
foo(vargs: 4, 5);

is there any way of achieving the above or should named arguments be avoided for varg?


Solution

  • Following on from insights in comments and an answer I've just added on a related question, it's possible to do this for an associative array.

    I found the key clue in the named parameters RFC, which says:

    Functions declared as variadic using the ...$args syntax will also collect unknown named arguments into $args.

    and:

    The foo(...$args) unpacking syntax from the argument unpacking RFC also supports unpacking named arguments

    Taken together, these mean that this is valid code (live demo):

    function foo($bar, ...$vargs) {
        var_dump($bar, $vargs);
    }
    
    foo(42, ...['a' => 1, 'b' => 2, 'c' => 3]);
    

    What happens here is that the unpacking turns the call into this:

    foo(42, a: 1, b: 2, c: 3);
    

    ...and then the "unknown named arguments" rule re-packs them into the $vargs parameter, with string keys, resulting in this output:

    int(42)
    array(3) {
      ["a"]=>
      int(1)
      ["b"]=>
      int(2)
      ["c"]=>
      int(3)
    }
    

    It turns out this is why foo(vargs: 4); works as well: vargs is not allowed as a named argument, but since any named parameter gets collected into the variadic argument, $vargs ends up as an associative array [ 'vargs' => 4 ]


    The key lesson is that you can use spread syntax to populate the variadic argument as long as:

    1. You use string keys in the array being unpacked.
    2. The names don't coincide with names of non-variadic parameters to the function.
    3. The function will correctly handle an associative array in the variadic parameter.

    If you have an existing list you want to spread, you could prepare it by prefixing each key to be a unique string. Casting to string is not enough, because arrays normalise numeric string keys back to integers (except in occasional edge cases that are considered bugs in PHP, and shouldn't be relied on).

    On the receiving end, you could use array_values to discard any resulting string keys in the variadic parameter, if what you actually wanted was a straight list.