Unpacked, named and positional arguments are in a call…mixed sorts of pumpkins

How to handle an array of positional arguments in PHP? Argument spread is the three dots operator, when used with arguments. Its primary usage is simple : it stands in front of a variable, and turns an array into a list of arguments.

<?php

$args = [1, 'a', E_ALL];
foo(...$args);

// Similar to 
foo(1, 'a', E_ALL);

?>

Recently, while checking the PHP 8.1 migration guide, I realized that named arguments after argument unpacking was introduced. It just does what it means :

<?php

foo(...$args, name: $arg);

?>

With PHP 8.1, it is possible to provide named arguments after the unpacked arguments. This is an edge case, as, nowadays, named parameters are still a small fraction of method calls. Yet, having this new feature mentioned in the migration guide made me wonder what are the other situations.

Unpacking arguments has to deal with three dots, positional arguments and named arguments. Also, this features is impacted by the structur of the unpacked arrays. Here is a rundown of the various cases, starting with the simpler cases.

Simple calls

Calling a method with an unpacked array is straighforward. Multiple unpacking may occur in a row : the arguments are then positioned one after the other. We’ll see about the content of the unpacked variables later.

<?php

foo(...$a, ...$b);

?>

Mixed calls

To combine unpacked variables and normal variables, there are some limitations : the normal arguments have to be put first. Just like for method declarations, three dots come last.

<?php

foo($a, ...$b);
foo(a: $a, ...$b);
foo(a: $a, ...$b, ...$c);
foo($a, $a2, ...$b, ...$c);

?>

Positionals arguments cannot be used after any three dots. It yields a compile time fatal error Cannot use positional argument after argument unpacking.

Until PHP 8.1, named arguments are also forbidden after the three dots. With PHP 8.1, they are allowed and fill their named paramter. This means that the initial unpacked array should not fill that position already.

<?php

// OK, $a and $b are filled
foo( ...[2], b: 0,);

// $a is filled by position 0 in the array, and named argument
//Fatal error : Named parameter $c overwrites previous argument
foo( ...[2], a: 0,);

function foo($a, $b) {
    print_r(func_get_args());   
}

Simple arrays

When the array uses integer as index, positional arguments are used.

<?php

function foo($a) {    print_r(func_get_args()); }

foo(...[1, 2]);
/*
Array
(
    [0] => 1
    [1] => 2
)
*/

?>

It is noteworthy that the order of the values is used, without any sorting on the keys. The indexes are simply dropped, and the values order is kept, like this :

<?php

foo(...[1, 2]);
foo(...[3 => 1, 1 => 2]);
foo(...['33' => 1, 2]);
/*
Array
(
    [0] => 1
    [1] => 2
)
*/

?>

Simple maps

When the array uses string as index, named arguments are used. It works with PHP 8.0 and more recent. Previous PHP versions yield a Fatal error : Cannot unpack array with string keys.

<?php

function foo($a, $b) {    print_r(func_get_args()); }

// both yield the same results
foo(...['a' => 1, 'b' => 2]);
foo(...['b' => 2, 'a' => 1]);
/*
Array
(
    [0] => 1
    [1] => 2
)
*/

?>

Named parameters are checked, including for case. Named parameters that do no exist are met with a Fatal error : Unknown named parameter $e.

Mixed array

It is possible to mix integer and strings index in the unpacked array. It follows the same rules than for arguments. Remember that the arguments are processed in the value order, just like if array_values() was applied, but string index were kept where available.

In particular, all the integer values must be at the first positions in the array, or a Fatal error Cannot use positional argument after named argument during unpackingis emited.

Similarly, named parameter cannot overwrite a previously filled spot. It yields a Named parameter $a overwrites previous argument.

<?php

function foo($a, $b) {    print_r(func_get_args()); }

// mixed positional and named. Good order, all OK.
foo(...[1, 'b' => 2]);
/*
Array
(
    [0] => 1
    [1] => 2
)
*/

// $a is filled with the first position, and then, again with the named paramter. Not possible
foo(...[2, 'a' => 1]);
// same as above, key is not important
foo(...[3 => 2, 'a' => 1]);

// positional arguments are not possible after a named one
foo(...['a' => 1, 3 => 2]);

?>

Three dots, positional and named parameters

Long are gone the days of simple positional arguments in a method call : nowadays, there are many ways to provide arguments to their right place : positional, named arguments and unpacked arrays.

Current usage reserve three dots usage to methods that accept arbitrary number of arguments. The arguments are all of the same sort, but their actual number is collected at execution time, like for array_merge(), for example.

Other calls tends to stick to one sort only : all positional or all named, with or without array unpacking. This keeps coding readable and simple. Otherwise, no less than four different Fatal errors await in the dark.

Using amy mixed approach with positional, named and unpacked arguments leads to the enforcement of several surprising rules, with errors emerging very late during execution : while overwriting a named parameter is detected at linting time, unpacking an array is only checked at call time.

As often, it is better to keep it simple.