Spreading the News – An Exploration of PHP’s Spread Operator

A few days ago, I wrote about array unpacking and variadic arguments in PHP8. I’m a big fan of these features, and use them extensively, and the changes in PHP8 make them even more useful.

Variadics were introduced in PHP 5.6: when you place the ... variadic (“splat”) operator in front of a function parameter in the function definition, the function will then accept a variable number of arguments, and the parameter will become an array inside the function containing the values of all those individual arguments. For example, this function to calculate the standard deviation from a series of values:


function stdev(int|float ...$values) {
    $mean = array_sum($values) / count($values);

    $sumSquares = array_reduce(
        $values,
        fn($carry, $value) => $carry + ($value - $mean) ** 2
    );
    
    return sqrt($sumSquares / count($values));
}

We can then call the stdev() function with any number of arguments:


echo stdev(2);                  // Gives 0.0
echo stdev(2, 3);               // Gives 0.5
echo stdev(9, 2, 5, 4, 12, 7);  // Gives 3.3040379335998

We might want to tighten up the logic in the function to handle an empty list of arguments, but otherwise it will work with any number of values.

Sometimes (mistakenly) called “array unpacking”, we can also pass an array of values when calling the function, also using the ... “spread” operator:


$values = [9, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4];

echo stdev(...$values);         // Gives 2.9832867780353

When we call a function using an array of values and the spread operator like this, PHP unpacks the array into a series of individual arguments that are then passed in through the function call. This doesn’t need to be a function or method that accepts variadic arguments, but can be any function, built-in or userland:


$arguments = [
    'now',
    new DateTimeZone('UTC'),
];

$now = new DateTimeImmutable(...$arguments);

Prior to PHP8, we still needed to pass in all the arguments for that function in the correct order; but PHP8 introduced two new change that I described in my previous article: named arguments, and support for associative arrays with the spread operator. Prior to PHP, the spread operator only worked with enumerated arrays.

Because the first argument to the DateTimeImmutable constructor is optional, with a default value of ‘now’, we can skip passing in that argument completely, and just pass in our timezone argument as a named argument, specifying the name as the string index in our array.


$arguments = [
    'timezone' => new DateTimeZone('UTC'),
];

$now = new DateTimeImmutable(...$arguments);

While using an array of arguments and the spread operator is really overkill for something basic like instantiating a new DateTimeImmutable object (just using a named timezone argument would be a lot simpler), this example should demonstrate the principle of the spread operator.

So even without considering its use with variadics, the spread operator is incredibly flexible.


I said that this was mistakenly called “array unpacking”: array unpacking takes the values from any iterable (not just from an array), and converts them to a set of values that can be passed to any operations that accept multiple arguments (not just arguments to a function call).

For example, the spread operator can be an alternative to calling array merge, to combine several arrays into one large array:


$values1 = [9, 2, 5, 4, 12];
$values2 = [7, 8, 11, 9, 3];
$values3 = [7, 4, 12, 5, 4];
$values4 = [10, 9, 6, 9, 4];

$allValues = [...$values1, ...$values2, ...$values3, ...$values4];

echo stdev(...$allValues);      // Gives 2.9832867780353

One thing that still disappoints me is that we can’t use the spread operator with list (or short list):


$values = range('A', 'E');

[$firstValue, $secondValue, ...$residualValues] = $values;

will result in a Fatal Error:

Fatal error: Spread operator is not supported in assignments

We can also apply the spread operator multiple times within a single call to a method or function, and it will unpack all the arrays into one series or arguments


$values1 = [9, 2, 5, 4, 12];
$values2 = [7, 8, 11, 9, 3];
$values3 = [7, 4, 12, 5, 4];
$values4 = [10, 9, 6, 9, 4];

echo stdev(...$values1, ...$values2, ...$values3, ...$values4);  // Gives 2.9832867780353

Despite the name “array unpacking”, the spread operator isn’t limited to arrays, but any iterable. We can also use it with the values returned from a Generator:


function squares(int $count) {
    for ($i = 1; $i <= $count; ++$i) {
        yield $i ** 2;
    }
}

echo stdev(...squares(4));  // Gives 5.6789083458003

or from an Iterator:


class fibonacci extends ArrayIterator {
    public function __construct($values) {
        parent::__construct($values);
    }
}

$fibonacci = new fibonacci([1,1,2,3,5,8,13]);

echo stdev(...$fibonacci);  // Gives 4.0957917676661

or an IteratorAggregate:


class fibonacci implements IteratorAggregate {
    private $values = [1,1,2,3,5,8,13];

    public function getIterator(): Traversable {
        return new ArrayIterator($this->values);
    }
}

$fibonacci = new fibonacci();

echo stdev(...$fibonacci);  // Gives 4.0957917676661

While squares and fibonacci are not particularly practical examples for passing to a function that calculates standard deviation, they serve to demonstrate that it isn’t only arrays that can be unpacked using the spread operator.


The biggest problem with array unpacking using the spread operators prior to PHP8 was that it would only work with enumerated arrays: using an associative array would throw a fatal error. PHP8 added support for associative arrays with the spread operator.


$array1 = ['A' => 1, 'B' => 2, 'C' => 3];
$array2 = ['D' => 4, 'E' => 5, 'F' => 3];

$allValues = [...$array1, ...$array2];

The spread operator has always renumbered the keys in an enumerated array: with the change implemented for PHP8, it now uses the same logic as array_merge() when combining associative or mixed arrays. That is, numeric keys will be renumbered with incrementing keys starting from zero; while string keys will be retained, and in the case of inputs having the same string keys, then the later value for that key will overwrite any previous one.
So:


$array1 = ['A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5];
$array2 = ['A' => 6, 'C' => 7, 'E' => 8];

$allValues = [...$array1, ...$array2];

results in an array:


array(5) {
  ["A"]=>
  int(6)
  ["B"]=>
  int(2)
  ["C"]=>
  int(7)
  ["D"]=>
  int(4)
  ["E"]=>
  int(8)
}

However, and rather oddly, having duplicate associative keys like this in multiple arrays unpacked in a function call will result in a fatal error:


$array1 = ['A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5];
$array2 = ['A' => 6, 'C' => 7, 'E' => 8];

function test(...$values) {
    var_dump($values);
}

test(...$array1, ...$array2);

Fatal error: Uncaught Error: Named parameter $A overwrites previous argument

To unpack these arrays for calling a function, we would have to do something like:


test(...[...$array1, ...$array2]);

which is an overhead, as PHP needs to build the intermediate array before unpacking that for the function call.

So while PHP8 gives a lot more flexibility when working with the spread operator and variadics than previous versions of PHP, although there are still some limitations.
Unserstanding that scope and flexibility is useful, as is knowing those limitations. Hopefully this article has served to broaden your own understanding of how the spread operator works, and how useful it can be.

This entry was posted in PHP and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s