skip to content

In PHP, how do you destructure nested arrays directly in a foreach loop, such as a route's list of latitude and longitude pairs?

level: middleimportance: should knowfreq 35%

answer

  1. the pattern replaces the value variable
  2. foreach ($points as [$lat, $lng])
  3. key => pattern keeps the key
  4. patterns nest to any depth
  5. same rules as plain destructuring

basics

~10 s

Write a pattern where the value variable goes: foreach ($track as [$lat, $lng]) unpacks each pair. Keys still work ($i => [$lat, $lng]), keyed patterns pick fields, and patterns nest to reach inner arrays.

solid answer

~40 s

In a `foreach` header, the value variable can be replaced by any destructuring pattern, so `foreach ($track as [$lat, $lng])` unpacks each `[lat, lng]` pair as the loop runs, and `list($lat, $lng)` works the same way. You can keep the outer key with `foreach ($routes as $slug => ['start' => $start])`, pick fields with keyed patterns, skip slots with empty positions, and nest patterns such as `['start' => [$lat, $lng]]` to reach into deeper arrays. The rules are those of a plain destructuring assignment: positional entries read keys `0`, `1`, `2`; keyed and unkeyed entries cannot mix inside one pattern level; and a missing key gives `null` with a warning, once per iteration.

code

php · 20 lines
php
<?php
declare(strict_types=1);

$route = [
    'name'  => 'Ridge Loop',
    'start' => [46.5580, 7.9812],
    'track' => [[46.5580, 7.9812, 2061], [46.5601, 7.9850, 2140], [46.5633, 7.9901, 2098]],
];

['name' => $name, 'start' => [$startLat, $startLng]] = $route;

$ascent = 0;
$previous = null;
foreach ($route['track'] as [, , $ele]) {
    if ($previous !== null && $ele > $previous) {
        $ascent += $ele - $previous;
    }
    $previous = $ele;
}
echo "$name from $startLat,$startLng climbs {$ascent} m", PHP_EOL; // climbs 79 m

go deeper

for a junior

Recall that foreach ($pairs as [$a, $b]) unpacks each element, and that $key => [$a, $b] keeps the key too.

for a middle

Explain that nested patterns follow the plain destructuring rules, including the per-level ban on mixing keyed and positional entries.

for a senior

Validate external data before a destructuring loop so a shape change does not flood logs with per-iteration warnings, and keep patterns shallow enough to read.

for a principal

Weigh loop-header destructuring against mapping rows into typed objects once; the former is lighter, the latter lets tooling check the shape everywhere it travels.

## The pattern takes the value's place A `foreach` loop normally assigns each element to a variable: `foreach ($track as $point)`. That value variable can be replaced by a **destructuring pattern**, and PHP unpacks each element into the pattern's variables on every iteration: ```php foreach ($track as [$lat, $lng]) { // one [lat, lng] pair per iteration } ``` It is exactly as if the loop body started with `[$lat, $lng] = $point;`. The `list($lat, $lng)` spelling is accepted in the same place. ## Forms you can use in a loop header | Header | What each iteration gets | |---|---| | `foreach ($track as [$lat, $lng])` | elements at keys 0 and 1 of each point | | `foreach ($track as [$lat, $lng, $ele])` | three positional values | | `foreach ($track as [, , $ele])` | only the elevation; two slots skipped | | `foreach ($track as $i => [$lat, $lng])` | the outer key plus the unpacked pair | | `foreach ($stops as ['name' => $name, 'lat' => $lat])` | named fields from each associative row | | `foreach ($routes as $slug => ['start' => [$lat, $lng]])` | outer key, and a nested pair inside a keyed field | ## Nesting patterns Patterns nest as deeply as the data does. Each nested pattern is independent, so one level can be keyed while another is positional: ```php ['start' => [$startLat, $startLng], 'end' => [$endLat, $endLng]] = $route; ``` The rule against mixing keyed and unkeyed entries applies **per level**: the outer pattern above is fully keyed, and each inner one is fully positional, which is allowed. Mixing both styles inside the same pair of brackets is a compile error. ## A hiking-route example A route API might return: - `name` — the route's title; - `start` and `end` — each a `[lat, lng]` pair; - `track` — a list of `[lat, lng, elevation]` triples. Computing the total ascent needs only the elevation of consecutive points, so the loop header can skip the coordinates entirely: `foreach ($route['track'] as [, , $ele])`. Plotting the line on a map needs the pairs: `foreach ($route['track'] as [$lat, $lng])`, which ignores the third element because a pattern only reads the keys it names. ## Pitfalls 1. **Wrong shape, repeated.** A missing key in a loop header raises the `Undefined array key` warning once per iteration, which can flood a log for a long track. Validate the shape once before the loop when the data is external. 2. **Associative rows under a positional pattern.** Rows fetched by column name have string keys; `foreach ($rows as [$id, $name])` reads keys `0` and `1` and gets `null`. Use `['id' => $id, 'name' => $name]` instead. 3. **Expecting changes to flow back.** Variables from a pattern hold copies of the values, so assigning to `$lat` inside the loop does not change `$track`. Writing back needs the loop's by-reference form or an explicit `$track[$i][0] = …`. 4. **Over-deep patterns.** A header that unpacks three levels at once is hard to read and harder to debug. Two levels is usually the limit before a small helper function or a typed object reads better. ## list() or [] in a loop header Both spellings are accepted and behave identically: - `foreach ($track as list($lat, $lng))` is the older form, still common in legacy code. - `foreach ($track as [$lat, $lng])` is the modern form and the one most style guides pick. - Nested patterns can use either spelling at each level, although mixing the two in one header reads badly. - Neither form changes what the loop iterates over; only how each element is split into variables. ## When to use it Loop-header destructuring shines when every element has the same small, known shape and the loop body uses the parts by name. It removes a line of `$lat = $point[0];` boilerplate per variable and makes the expected shape visible at the top of the loop, where a reviewer looks first.

  • Can one foreach header both keep the array key and unpack the value?
    Yes. Write `foreach ($routes as $slug => [$lat, $lng])`: the key goes into `$slug` exactly as usual, and the value is unpacked by the pattern. The key itself cannot be destructured, because keys are only ever ints or strings.
  • Why can ['start' => [$lat, $lng]] = $route compile when mixing keyed and positional entries is forbidden?
    The rule applies inside one pair of brackets. The outer pattern contains only a keyed entry, and the inner pattern contains only positional entries, so neither level mixes styles. `['start' => $s, $other] = $route` would fail, because that single level mixes both.

saying these in an interview costs you the question

  • foreach cannot destructure; you must unpack inside the loop body
  • Using a pattern in foreach drops the outer array key
  • Nested patterns must all be keyed if the outer one is
  • Assigning to a destructured loop variable updates the source array
  • A positional pattern works on rows fetched by column name