Named arguments and variadic in PHP 8

PHP 8.0 introduces the named arguments. This is an extension of the current positional syntax. Named arguments removes some of the limitations. It also modernizes the way methods are called with an arbitrary number of arguments, with the spread operator. Let’s see what is happening there.

Quick review of the Named parameters

Until the last 7.x versions, PHP used only positional parameters. The argument at the first position is assigned to the parameter at the first position, in the method signature. Any missing argument is then filled with the default value. Finally, superfluous arguments are dropped, or a message is raised to signal that parameters are missing.

 
<?php

function foo($a, $b = 1) { echo "$a / $b\n"; }

// Not enough parameter 
foo();

// Enough parameters 
foo(1); foo(1, 2);

// Too many parameters 
foo(1, 2, 3);

?>

The problem arises with optional parameters. In particular when there are many of them and they are all independent. The positional arguments impose that all parameters are provided before using the default value, and thus, may be skipped. When one option has to be set, and it is lying at the end of the parameters list, all the previous values must be filled.

 
<?php

function foo($a = 0, $b = 1) {}

// Default behavior foo();

// Set to 1-1 
foo(1);

// Set to 1-0, so inverted 
foo(1, 0);

// Set to b = 2, don't change a. 
foo( 0 /* default would be good */, 2);

?> 

In native PHP, it happens with functions like setcookie(), mail(), or jsondecode(). Sometimes, the problem is skipped with a dedicated option function, like sessionstart() and an array of options.

For those functions, the list of arguments is long : it is frequent that we, coders, have to rely on the documentation to know which argument has to be passed. In fact, not only the target parameter must be known, but also all the previous ones, so as to fill them with their correct default values.

Named Parameters

Named parameters solve this problem by giving a name to each parameter. Indeed, they already have a name, since the signature assigns a distinct name to each argument. That was not the case until some not-so recent version, but it is now enforced at linting time.

The call syntax changes a little, to accommodate the target name. It is the name of the argument in the signature, without the classic $ dollar sign. Be aware that they are case sensitive.

 
<?php

function foo($a = 0, $b = 1) {}

// Default behavior foo();

// Set to 1-0 
foo(a: 1);

// Set to 1-2 
foo(a: 1, b: 2);

// Set to b = 3 
foo( b: 3);

?> 

From now one, the arguments are identified by their name, not their position. So, values may be assigned to the right argument, and the calling order may be different from the method signature.

As you can see, the name of the argument is a kind of in-the-code documentation. This makes the 7th argument of setcookie() obvious : it is httponly.

Named parameters and spread operator

The spread operator is the opposite of the variadic. They share the same syntax : ... and they have opposite effects. In a signature, ... means that all remaining arguments are collected in an array, while in a methodcall, it means that all elements of the array are used, respective of their position in the array.

This PHP 8.0 code illustrates the feature.

 
<?php

function foo($a = 0, $b = 1) {}

// Set to 1-1 foo(...[1, 1]);

// Set to 1-1 foo(...[4 => 1, 1]);

// Set to 1-2 
foo(...['a' => 1, 'b' => 2]);

// An error, as $a receives the whole array. Typehint this! 
foo([1, 2]);

?>
 

As you can see, two syntax of arrays are possible : the positional and the named parameters. Let’s review them both.

Positional arguments use the integer index, in particular the auto-generated index. When an array is filled without mentioning indexes, PHP auto-generates an index, starting at 0. (There are some subtleties around this generation, so check the docs about arrays).

Positional arguments and index order

The positional arguments are based on a simple PHP array. It is straightforward : the n-th element of the array is given to the n-th argument in the method.

 
<?php

function foo($a, $b, $c) { print "$a-$b-$c\n"; }

// Set to 0-1-2 
foo(...range(0, 2));

// Set to 0-1-2 
foo(...array(4 => 0, 1, 2));

?> 

It also means that the index in the array is not the position of the argument. As you can see above, when the array’s indexing start at 4, the arguments are still delivered in the same order : 0, 1, 2. PHP doesn’t take into account the index, but the position of the elements in the array. This is the way their were introduced in the array.

Let’s push this example a little further :

 
<?php

function foo($a, $b, $c) { print "$a-$b-$c\n"; }

// Set to 0-1-2 
foo(...array(4 => 0, -3 => 1, 12 => 2));

// Set to 1-0-2 
$x = array(4 => 0, -3 => 1, 12 => 2); 
ksort($x); 
foo(...$x);

?>
 

The order of the arguments remains the same, whatever the actual order of the indexes in the array. Of course, we could use the index order, by calling ksort() on the array, to change the positions before using it in a call. That way, PHP reorders the elements according an arbitrary order, and then, use the array as arguments.

The actual order, that PHP uses, is the one available with array_values(). This is a subtle distinction, in particular when the list of arguments is built with an algorithm. When doing so, it is recommended keeping a consistent way to fill the array, or use the index to sort the arguments prior to the call.

Spread hash, only for arguments

Of course, the best way to sort is to let PHP do it by using the named parameters. As you may have noticed in the first examples, using a hash for argument is possible.

Hashes are also called maps, in other languages. It is the version of the PHP array, which uses strings as index. Here, hashes are just what we need to provide the names of the arguments, and their value. So, it was adapted to PHP 8.0, for calling named argument. Be aware that it is not possible to use ... with hashes in arrays.

 
<?php

function foo($a, $b) { print "$a-$b\n"; }

// Set to 1-2 
foo(...[1, 2]);

// Set to 1-2 
foo(...['a' => 1, 'b' => 2]);

// Set to 1-2 
foo(...['b' => 2, 'a' => 1]);

//Fatal error: Cannot unpack array with string keys 
[...[1, 2, 'd' => 2], [4]];

?> 

Back to the named arguments. As long as the string index match the arguments, everything is fine. Just remember to drop the $ in the index name, as it is not needed.

Extra arguments are signaled with a Fatal Error : Unknown named parameter $d. Missing arguments are filled with their default value, when available, then signaled with an error too : Too few arguments to function foo().

All that is classic. The next is new.

Going Full Hashes!

Hashes match very closely the arguments passing method. All the names are unique in the hash index, just like the method’s arguments. No double ['a' => 1, 'a' => 2], as PHP silently overwrites the first with the second.

An interesting segue is the question of the variadic operator. Since, multiple indexes are not possible, a then natural solution is to consolidate all the values of one argument in an array. After all, this is what the ... operator does, when it is used in the method signature.

 
<?php

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

foo(3, 4); foo(...[2, 'b' => [2, 3]]);

?> 
Array
(
    [0] => 4
)
Array
(
    [b] => Array
        (
            [0] => 2
            [1] => 3
        )

)

There are now 2 nuances between the calls : first, the received argument changes its shape depending on the calling syntax.

The first call is the hardcoded version. Upon reception, $b is an array, with one element : 4. This is expected. Adding a third argument to the call will add this value after the 4.

The second call is the positional dynamic version. Upon reception, $b is now an array of array, and the actual arguments are at index [0]. This is backward compatible with PHP 7.2, which is also behaving like that. Still, this means that the way to call a method may have an impact on the format of the received arguments.

Variadic Arguments Are Now Named

PHP 8.0 introduces a new change : the resulting array is now a hash too. It is not an array, with an integer as index. It is a hash, where the name of the only index is the name of the parameter.

 
<?php
function foo($a, ...$b) { print_r($b); }

foo(...[2, [2, 3]]); foo(...[2, 'b' => [2, 3]]);

?>
 
Array
(
    [b] => 1
)
Array
(
    [b] => Array
        (
            [0] => 2
            [1] => 3
        )

)

The index 0 has become the index b. This letter is the name of the argument, and it doesn’t change with the calling syntax. This time, it is a backward incompatible change : the code that fetch the argument value must access the index b and not 0.

One fix to this is to apply array_values() on the incoming argument, so as to fall back on the previous behavior. Otherwise, the argument might be merged by array_merge() with other arrays, and the key will travel across the application. array_merge() and some other PHP native functions, preserve the keys.

Named arguments and variadic

Named arguments are new in PHP 8.0, and they already make an impact on our code. Instead of using arrays of options, it is now possible have them all in the method signature, and skip them as needed. This is definitely an improvement. Backward incompatibility, like the indexing of variadic arguments, is going to require some adaptation.

Giving readable argument names is going to the other impact. Until now, the argument names were limited to the method’s realm, and they had no visibility outside. With PHP 8.0, it is important to give a meaningful name to the arguments, so they can act as documentation, and be easy to remember.

You can get ready for PHP 8.0, by running Exakat on your code base. Here are some recommended reading :

  • Parameter names report, which lists all parameters in the application, with their typehint and values, for a check on spelling, meaning and consistency
  • Named Parameter Variadic, a rule that checks variadic parameters as they go into PHP 8.0
  • Mismatch Parameter Name, a rule that checks if parameter names are in synch across class extensions and interface implementations
  • Swapped arguments, a rule that checks when arguments have exchanged their position between overwritten methods.

Thanks to Ciaran McNulty for spotting a lot of those, while working on Behat’s PHP 8.0 version.