0

Array unpacking and spread operator (...$array)

Intermediate5 min read·php-04-005
compare

Concept

PHP 8.1 extended the spread operator ... to work with string-keyed arrays in function calls and array literals. Before 8.1, spreading was limited to numerically-indexed arrays. The spread operator in array context ([...$a, ...$b]) is a concise alternative to array_merge(), but with important semantic differences regarding key handling.

When spreading a numerically-indexed array into another array, keys are reindexed sequentially. When spreading an associative array (PHP 8.1+), the string keys are preserved, and later values with the same key overwrite earlier ones — identical to how array_merge() handles string keys. The key difference from array_merge() is that the + union operator preserves the first occurrence of a key, while array_merge() and spread both preserve the last.

Array unpacking in function arguments is the original use case: f(...$args) passes each element of $args as a separate positional argument. PHP 8.1 also added spread for string-keyed arrays in function calls, which maps to named arguments. This is powerful for building argument lists dynamically — for example, when wrapping a function call and forwarding arguments.

A subtle performance note: the spread operator in array literals ([...$a, ...$b]) creates a new array and copies all values. It is not O(1). For very large arrays, array_merge($a, $b) and [...$a, ...$b] have equivalent O(n+m) cost. The spread syntax is preferred for readability. Avoid spreading inside hot loops where $arr[] = $value or array_push() would suffice.

OperationString key behaviorNumeric key behavior
[...$a, ...$b]Last write winsReindexed from 0
array_merge($a, $b)Last write winsReindexed from 0
$a + $bFirst write winsFirst write wins
array_replace($a, $b)Last write wins (recursive available)Last write wins

Code Example

php
<?php
declare(strict_types=1);

// ---- Numeric spread: reindexed ----
$first  = [1, 2, 3];
$second = [4, 5, 6];
$merged = [...$first, ...$second];
// [1, 2, 3, 4, 5, 6] — keys 0..5

// ---- Compare: + operator keeps first value ----
$a = [1, 2, 3];
$b = [10, 20, 30, 40];
$plus   = $a + $b;    // [1, 2, 3, 40]  — $a values win for keys 0,1,2
$spread = [...$a, ...$b]; // [1, 2, 3, 10, 20, 30, 40] — all 7 elements

// ---- Associative spread (PHP 8.1+) ----
$defaults = ['timeout' => 30, 'retries' => 3, 'verbose' => false];
$overrides = ['timeout' => 60, 'verbose' => true];

$config = [...$defaults, ...$overrides];
// ['timeout' => 60, 'retries' => 3, 'verbose' => true]
// overrides wins on duplicate string keys

// ---- Spreading into function call ----
function createUser(string $name, int $age, string $role = 'viewer'): array
{
    return compact('name', 'age', 'role');
}

$args = ['name' => 'Alice', 'age' => 30, 'role' => 'admin'];
$user = createUser(...$args); // maps to named arguments in PHP 8.1+

// ---- Spread in the middle of a literal ----
$base    = ['a' => 1, 'b' => 2];
$extra   = ['c' => 3];
$full    = ['prefix' => 0, ...$base, ...$extra, 'suffix' => 99];
// ['prefix' => 0, 'a' => 1, 'b' => 2, 'c' => 3, 'suffix' => 99]

// ---- Forwarding variadic arguments ----
function log(string $level, string ...$messages): void
{
    foreach ($messages as $msg) {
        echo "[$level] $msg\n";
    }
}

$messages = ['Connected', 'Query executed', 'Response sent'];
log('INFO', ...$messages); // spreads $messages as positional args

// ---- Practical: merging config layers ----
$env     = ['db_host' => 'prod-db', 'cache' => 'redis'];
$local   = ['db_host' => 'localhost']; // local overrides env
$merged  = [...$env, ...$local];
// ['db_host' => 'localhost', 'cache' => 'redis']

Interview Q&A

Q: What is the difference between [...$a, ...$b] and $a + $b when both arrays share the same string keys?

The spread operator ([...$a, ...$b]) and array_merge() both implement "last write wins" — when $b has a key that already exists in $a, $b's value overwrites $a's value. The + (union) operator implements "first write wins" — keys already present in $a are not overridden by $b. For numeric keys, the distinction is even sharper: + preserves numeric keys as-is and skips duplicates, while spread reindexes all numeric keys to produce a contiguous sequence. Use + when you want defaults that can be overridden by specific values, and spread or array_merge() when the later array should take precedence.


Q: How does spreading a string-keyed array into a function call work in PHP 8.1, and what restriction existed before that version?

Before PHP 8.1, the spread operator in function calls only worked with numerically-indexed arrays, mapping elements to positional parameters by index. Spreading ['name' => 'Alice', 'age' => 30] into a function call would throw a TypeError because PHP could not map string keys to positional parameters. PHP 8.1 lifted this restriction by mapping string keys to named arguments. The function call f(...['name' => 'Alice', 'age' => 30]) is equivalent to f(name: 'Alice', age: 30). This enables powerful patterns like building argument bags from config arrays or forwarding named arguments through wrapper functions.


Q: In a performance-critical loop that builds a large array by merging small arrays on each iteration, should you use [...$result, ...$chunk] or array_push($result, ...$chunk)? Why?

Use array_push($result, ...$chunk) or foreach ($chunk as $v) { $result[] = $v; }. The expression $result = [...$result, ...$chunk] creates a completely new array on every iteration, copying all O(n) existing elements and all O(k) new elements. After 100 iterations of 100-element chunks, you have made 100 full array copies for a total of O(n²) work. array_push($result, ...$chunk) appends to the existing HashTable in O(k) amortized time per iteration, for a total of O(n) — a dramatic difference for large datasets. For merging many arrays, array_merge(...$chunks) called once after collecting all chunks is also O(n) and idiomatic.