In PHP, how does the ... spread operator work inside an array literal, and what changed for string keys in PHP 8.1?
answer
- expands arrays and Traversables in place
- integer keys renumbered, appended
- string keys: later value wins
- string keys allowed since 8.1
- not allowed on the left of =
basics
~10 s[...$a, ...$b] copies the elements of arrays or Traversables into a new array: integer keys are renumbered, and since PHP 8.1 string keys are kept with later values overwriting earlier ones, like array_merge().
solid answer
~40 sInside an array literal, `...$x` expands `$x` in place, and you can spread several sources and mix them with normal elements: `[...$defaults, 'unit' => 'km', ...$overrides]`. The source must be an array or a `Traversable` such as a generator; anything else throws an `Error`. **Integer keys are not kept**: those elements are appended with fresh indexes. **String keys are kept**, and when two sources share one, the later value wins, the same result `array_merge()` gives. Spreading arrays with string keys has been allowed since PHP 8.1; PHP 7.4 and 8.0 threw "Cannot unpack array with string keys". Spread only builds arrays: `[$first, ...$rest] = $items` is a compile error.
code
php · 18 lines<?php
declare(strict_types=1);
$defaults = ['unit' => 'km', 'precision' => 4];
$overrides = ['precision' => 6];
$options = [...$defaults, ...$overrides, 'source' => 'api'];
// ['unit' => 'km', 'precision' => 6, 'source' => 'api']
$a = [5 => [46.558, 7.981]];
$b = [5 => [46.563, 7.990]];
$track = [...$a, ...$b]; // keys 0 and 1: integer keys are renumbered
function points(): Generator {
yield [46.558, 7.981];
yield [46.560, 7.985];
}
$fromGenerator = [...points()];
echo count($track), ' ', count($fromGenerator), ' ', $options['precision'], PHP_EOL; // 2 2 6go deeper
Recall that [...$a, ...$b] builds a new array from both, and that it works with string keys since PHP 8.1.
Explain the key rules: integer keys renumbered and appended, string keys kept with later values winning, the same as array_merge().
Spot the quadratic copy of spread-accumulating in a loop, guard optional sources with ?? [], and know spread throws on null where destructuring does not.
Set a team idiom for merging configuration and lists so later-wins semantics are consistent, and flag PHP 8.0-era array_merge workarounds that can now be simplified.
## What spread does in an array literal The **spread operator** `...` placed before an expression inside an array literal expands that expression's elements into the array being built. It has worked for integer-keyed arrays since PHP 7.4: ```php $segment1 = [[46.5580, 7.9812], [46.5601, 7.9850]]; $segment2 = [[46.5633, 7.9901]]; $track = [...$segment1, ...$segment2]; // three points, keys 0, 1, 2 ``` You can spread any number of sources and put ordinary elements before, between or after them. ## How keys are treated | Source key | What the new array gets | |---|---| | integer key (`0`, `7`, …) | the value is appended at the next free integer index; the original key is dropped | | string key (`'unit'`) | the key is kept; a later occurrence overwrites the earlier value, keeping its original position | This is the same rule `array_merge()` follows, and it is what makes spread useful for options arrays: ```php $options = [...$defaults, ...$userOptions]; // user values win on shared keys ``` An explicit element written after a spread also overwrites: `['unit' => 'm', ...$prefs, 'unit' => 'km']` ends with `'unit' => 'km'`. ## The PHP 8.1 change In PHP 7.4 and 8.0, spreading an array that contained any string key threw an `Error`: "Cannot unpack array with string keys". Code of that era used `array_merge()` for associative data and spread only for lists. Since **PHP 8.1** string keys are supported, so the two are interchangeable for merging arrays, and spread reads naturally when literal elements are mixed in. ## What can be spread - **Arrays**, the usual case. - **Traversable objects**, including generators and `ArrayIterator`. Their keys follow the same rules; a key that is neither an `int` nor a `string` throws an `Error` ("Keys must be of type int|string during array unpacking"). - **Anything else** (`null`, an `int`, a string) throws an `Error`: "Only arrays and Traversables can be unpacked". A `null` from a failed lookup therefore fails loudly here, unlike in destructuring. Spread also works in **constant expressions**, so `const ALL_ZONES = [...ALPINE_ZONES, ...JURA_ZONES];` is valid when both are constant arrays. ## Where spread is not allowed Spread only **builds** arrays. On the left of `=`, `[$first, ...$rest] = $items;` fails at compile time with "Spread operator is not supported in assignments"; use `array_slice()` for the tail. Spreading *arguments* into a function call (`f(...$args)`) is the same token but a different feature, with its own rules for named arguments. ## Version history | Release | Spread in array literals | |---|---| | PHP 7.4 | introduced; arrays and Traversables with integer keys only | | PHP 8.0 | unchanged; a string key still throws "Cannot unpack array with string keys" | | PHP 8.1 | string keys allowed, later value wins | ## Traps interviewers probe - **Lists never collide.** Two lists that both use keys `0..n` are concatenated, not overwritten, because integer keys are dropped. - **Numeric-string keys count as integers.** A generator yielding the key `"42"` has that key treated as the integer `42`, so the value is appended with a fresh index rather than kept under `"42"`. - **Order follows the literal.** Elements appear in the order the sources and literal elements are written; a later string key overwrites in place, it does not move to the end. - **Empty sources are fine.** Spreading `[]` adds nothing, which is why `...($extra ?? [])` is a safe guard for an optional source. ## Costs and habits Each literal builds a **new** array and copies every spread element into it. That is fine for a handful of merges, but `$all = [...$all, ...$batch];` inside a loop copies the growing array on every iteration, so the total work grows with the square of the result size. Collect batches in a list and spread once at the end, or append in place. | Goal | Idiom | |---|---| | concatenate lists | `[...$a, ...$b]` | | merge options, later wins | `[...$defaults, ...$overrides]` | | materialise a generator into a list | `[...$generator]` (string keys kept, int keys renumbered) | | add one element at the front | `[$head, ...$list]` |
- Why does [...[5 => 'a'], ...[5 => 'b']] contain both values?Integer keys are never kept by spread; each value is appended at the next free index. The result is `[0 => 'a', 1 => 'b']`. Only string keys collide and overwrite, which is why spread concatenates lists but merges maps.
- What happens if you spread a variable that turns out to be null?The literal throws an `Error`: "Only arrays and Traversables can be unpacked, null given". That is stricter than destructuring `null`, which silently yields nulls. When a source is optional, spread `$x ?? []` so a missing value contributes nothing.
saying these in an interview costs you the question
- Spread keeps integer keys like the + operator
- Spreading an array with string keys throws even in PHP 8.5
- With duplicate string keys the first value wins
- Spread can collect the rest in a destructuring pattern
- Only arrays can be spread; generators need iterator_to_array()