PHP Arrays: Ordered Maps, References, and the array_* Toolkit
Understand PHP's array as an ordered map — indexed, associative, and nested — including key behavior, references, unpacking, and gain hands-on fluency with the core array_* functions used for transforming, filtering, and querying data.
PHP's array is not a single simple data structure — it's an ordered hash map that can act as a list (indexed array), a dictionary (associative array), a set, a stack, a queue, or a tree (nested arrays), all with the same syntax. That flexibility is powerful but means you need a clear mental model of how keys, order, and references actually work, plus fluency with the large array_* function library, which is where the vast majority of real-world PHP data manipulation happens.
Indexed, Associative, and Nested Arrays
An indexed array uses sequential integer keys starting at 0 by default: $colors = ['red', 'green', 'blue']; is shorthand for [0 => 'red', 1 => 'green', 2 => 'blue']. An associative array uses explicit, meaningful keys — strings or arbitrary integers — instead: $person = ['name' => 'Ada', 'age' => 36];. There is no separate 'associative array' type in PHP; every array is the same underlying ordered-map structure, and whether you treat it as a list or a dictionary is purely a matter of which keys you use and how you iterate it.
Arrays can nest arbitrarily: $company = ['name' => 'Acme', 'employees' => [['name' => 'Ada', 'role' => 'Engineer'], ['name' => 'Grace', 'role' => 'Admiral']]]; models a tree of data, and this is exactly the shape you get back from json_decode($json, true) when parsing a JSON API response — nested associative and indexed arrays mirroring the JSON's objects and arrays.
Critically, PHP arrays are *ordered maps* — insertion order is preserved and is exactly what foreach walks through, regardless of the keys used. This is a real, load-bearing guarantee (unlike, say, a plain JavaScript object before ES2015 formalized ordering), and it's why you can reliably build things like an ordered list of steps using string keys without worrying about the order scrambling.
• $list = [10, 20, 30]; — indexed array, keys 0, 1, 2 assigned automatically. • $map = ['host' => 'localhost', 'port' => 5432]; — associative array with explicit string keys. • $tree = ['a' => ['b' => ['c' => 'deep value']]]; — arbitrarily nested arrays. • foreach() always walks arrays in insertion order, never key-sorted or type-sorted order.
Keys, References, and Unpacking
Array keys can be integers or strings; a numeric string key like '5' is automatically cast to the integer 5. When you don't specify a key, PHP assigns the next integer after the current highest integer key used so far — so after $a = [5 => 'x']; $a[] = 'y';, the new element lands at key 6, not 0. isset($arr['key']) checks existence and non-null in one call; array_key_exists('key', $arr) checks existence even if the value is explicitly null — the two are not interchangeable when null values are meaningful in your data.
By default, PHP copies arrays by value: assigning one array variable to another ($b = $a;) creates an independent copy, and modifying $b does not affect $a — this 'copy-on-write' semantics is implemented efficiently under the hood (PHP defers the actual copy until one of the two is mutated) but behaves, from your code's perspective, exactly like a full copy. To share a single array between variables (or pass one into a function and have it mutated in place), you use references explicitly with &: $b = &$a; makes $b an alias for the same array, and function addItem(array &$list, $item) { $list[] = $item; } mutates the caller's array directly because the parameter is declared by reference.
The spread operator ... unpacks an array's elements in place — inside another array literal ($combined = [...$a, ...$b];, PHP 7.4+, and since PHP 8.1 this also works with string keys) or as a function's arguments (max(...$numbers) spreads an array of numbers as individual arguments to max()). List assignment, via [$a, $b] = $pair; or the older list($a, $b) = $pair;, destructures an indexed array's values into separate variables in one line, and can also destructure by key: ['name' => $n, 'age' => $a] = $person;.
• isset($data['key']) is false if the value is null; array_key_exists('key', $data) is true even for a null value. • function push(array &$arr, $val) { $arr[] = $val; } — pass-by-reference parameter mutates the caller's array. • $merged = [...$defaults, ...$overrides]; — spread unpacking, later values win on key collision. • ['id' => $id, 'name' => $name] = $row; — keyed destructuring.
Transforming Data: map, filter, reduce, walk
array_map(callable $fn, array $arr) applies $fn to every element and returns a brand-new array of the results, the same length as the input, with keys preserved for single-array calls. It's the go-to tool for 'transform every item the same way': array_map(fn($p) => $p['price'] * 1.1, $products) returns a new array of prices with 10% added, without touching the original.
array_filter(array $arr, ?callable $fn = null) keeps only the elements for which $fn returns a truthy value (or, with no callback at all, keeps only truthy elements) — and crucially it *preserves original keys*, which means the result often needs array_values() afterward if you need a clean re-indexed list rather than a gappy one. array_reduce(array $arr, callable $fn, $initial = null) collapses an entire array down to a single accumulated value by repeatedly calling $fn($carry, $item) — it's how you compute sums, build lookup maps, or flatten structures in a single pass.
array_walk(array &$arr, callable $fn) differs from the others: it mutates the array *in place* (hence the &$arr by-reference parameter) by calling $fn($value, $key) for each element rather than returning a new array — use it when you specifically want in-place mutation, and array_map when you want a fresh transformed copy without side effects (which is almost always the better default in modern code).
• array_map(fn($n) => $n * $n, [1, 2, 3]); // [1, 4, 9] • array_filter($users, fn($u) => $u['active']); // keeps original keys, may need array_values() • array_reduce([1,2,3,4], fn($carry, $n) => $carry + $n, 0); // 10 • array_walk($items, function (&$item) { $item = strtoupper($item); }); // mutates in place, note &$item
Querying, Combining, and Slicing
array_column($records, 'name') pulls a single column out of an array of arrays/objects — extremely common for turning a list of database rows into a flat list of one field, and its 3-argument form array_column($records, 'name', 'id') builds an associative id-to-name lookup map in one call. array_merge($a, $b) combines arrays, renumbering integer keys but letting later arrays' string-keyed values overwrite earlier ones; array_replace($a, $b) is similar for string/mixed keys but preserves integer keys instead of renumbering them — the choice between +, array_merge(), and array_replace() depends entirely on whether you want integer keys renumbered and which side should win on collision.
array_search($needle, $haystack) returns the *key* of the first matching value (or false, compare with ===), while in_array($needle, $haystack) just returns whether it exists at all — both accept a third strict boolean argument to force === comparisons internally. Sorting comes in many flavors: sort()/rsort() re-index and sort by value ascending/descending; asort()/arsort() sort by value but preserve keys; ksort()/krsort() sort by key; and usort()/uasort()/uksort() take a custom comparison callback (typically written with the spaceship operator) for anything beyond the built-in orderings.
array_slice($arr, $offset, $length) returns a portion of an array without modifying the original (like a read-only window), while array_splice($arr, $offset, $length, $replacement) removes and optionally replaces a portion *in place*, mutating the original array and returning the removed elements — this pair is exactly analogous to a read-only vs. mutating substring operation, but for arrays. array_chunk($arr, $size) splits an array into an array of smaller arrays of at most $size elements each (handy for pagination or batch processing), and array_unique($arr) removes duplicate values, comparing them as strings by default and preserving the first-seen key for each value.
• array_column($rows, 'email', 'id'); // ['1' => '[email protected]', '2' => '[email protected]', ...] • array_merge(['a'=>1,0=>'x'], ['a'=>2,0=>'y']); // ['a'=>2, 0=>'x', 1=>'y'] — int keys renumbered • array_replace(['a'=>1,'b'=>2], ['b'=>9]); // ['a'=>1,'b'=>9] — keys preserved, not renumbered • array_slice($arr, -3); // last 3 elements, non-destructive • array_splice($arr, 1, 2, ['X']); // removes 2 elements starting at index 1, inserts 'X' in their place, mutates $arr • array_chunk(range(1, 10), 3); // [[1,2,3],[4,5,6],[7,8,9],[10]]
A pipeline built from array_* functions
array_filter() preserves original keys — wrap in array_values() if you need a clean re-indexed list afterward. array_map() always returns a new array; array_walk() mutates the original array in place (its callback parameter must be by reference to have any effect). usort() re-indexes the array; uasort() sorts by value while preserving keys, for when the keys matter.
<?php
$orders = [
['id' => 1, 'customer' => 'Ada', 'total' => 120, 'status' => 'paid'],
['id' => 2, 'customer' => 'Grace', 'total' => 45, 'status' => 'pending'],
['id' => 3, 'customer' => 'Ada', 'total' => 300, 'status' => 'paid'],
];
$paidTotal = array_reduce(
array_filter($orders, fn($o) => $o['status'] === 'paid'),
fn($carry, $o) => $carry + $o['total'],
0
);
$byCustomer = array_column($orders, 'total', 'customer'); // last one wins per key here
usort($orders, fn($a, $b) => $b['total'] <=> $a['total']); // highest total first
echo $paidTotal; // 420Code Examples
This is the idiomatic modern-PHP way to process a list: array_map transforms each item into a derived value without mutating the source, array_filter keeps only the items meeting a condition (while preserving their original keys), and array_reduce collapses a list down to one summary value — three composable, side-effect-free operations chained together instead of one imperative loop doing everything at once. Expected result: Grand total: $54.47 Items in cart: 2
$grandTotal = array_reduce($lineTotals, fn($sum, $t) => $sum + $t, 0);
array_column's third argument builds an id-keyed lookup map in a single call, which is far cleaner than a manual foreach loop for that specific job. array_walk mutates the original array element-by-element in place — note the callback parameter must be declared by reference (&$user) or the mutation would be lost, since PHP would otherwise pass a copy into the callback. Expected result: Array ( [101] => ada [102] => grace) Array ( [0] => Ada [1] => Grace) array_walk($users, function (&$user) {... }); // &$user is required for the mutation to stick
<?php
$cart = [
['name' => 'Widget', 'price' => 9.99, 'qty' => 3],
['name' => 'Gadget', 'price' => 24.50, 'qty' => 1],
['name' => 'Gizmo', 'price' => 3.00, 'qty' => 0],
];
$lineTotals = array_map(fn($item) => $item['price'] * $item['qty'], $cart);
$inStockLines = array_filter($cart, fn($item) => $item['qty'] > 0);
$grandTotal = array_reduce($lineTotals, fn($sum, $t) => $sum + $t, 0);
printf("Grand total: $%.2f\n", $grandTotal);
printf("Items in cart: %d\n", count($inStockLines));<?php
$users = [
['id' => 101, 'name' => 'ada'],
['id' => 102, 'name' => 'grace'],
];
$byId = array_column($users, 'name', 'id');
print_r($byId); // [101 => 'ada', 102 => 'grace']
array_walk($users, function (&$user) {
$user['name'] = ucfirst($user['name']);
});
print_r(array_column($users, 'name'));array_slice vs array_splice
slice reads a window without touching the source; splice removes (and optionally replaces) that window in the original array itself.
original: [ a, b, c, d, e ]
array_slice($arr, 1, 2) -> returns [b, c] ; original array UNCHANGED
array_splice($arr, 1, 2) -> returns [b, c] ; original array becomes [a, d, e] (MUTATED)
array_splice($arr,1,2,['X'])-> original becomes [a, X, d, e] (removed + replaced in place)Important Rules
Prefer array_map/array_filter/array_reduce (pure, no side effects) over array_walk unless in-place mutation is specifically what you need.
Remember array_filter() preserves original keys — call array_values() if you need a clean re-indexed result.
Use array_column() to extract a single field or build an id-keyed lookup map instead of a manual foreach loop.
Choose array_merge() when integer keys should renumber, array_replace() when integer keys must be preserved, and the + operator when the left array's values should win on collision.
Always compare array_search()/strpos()-style 'position or false' results with ===, never == or a truthiness check.
Where this shows up
Any code that shapes an API response, prepares data for a view/template, or aggregates database results leans on this exact toolkit — array_column() to pull ids for a follow-up query, array_map()/array_filter() to reshape rows into DTOs, usort() with a spaceship comparator for custom ordering, and array_chunk() to batch large datasets for processing or pagination.
$active = array_filter($users, fn($u) => $u['active']); echo $active[0]['name']; // may not exist — filter kept original keys, index 0 might have been filtered out
$active = array_values(array_filter($users, fn($u) => $u['active'])); echo $active[0]['name']; // safe — re-indexed from 0 array_filter() preserves the original array's keys rather than renumbering them, so if the element that used to be at index 0 gets filtered out, index 0 simply won't exist in the result — accessing it directly can throw an undefined-array-key warning. Wrapping with array_values() produces a clean, gap-free 0-based array.
array_walk($items, function ($item) { $item = strtoupper($item); }); // no effect — $item is a copy array_walk($items, function (&$item) { $item = strtoupper($item); }); // & makes it mutate in place
Without the & on the callback's parameter, PHP passes a copy of each element into the closure, so modifying $item inside the callback has zero effect on the original $items array — array_walk's entire purpose (in-place mutation) requires the by-reference parameter.
Which function mutates the original array in place, rather than returning a new one? A. array_map() B. array_filter() C. array_walk() D. array_column()
C. array_walk() — array_walk() takes the array by reference and calls the callback for each element, mutating the original array directly (provided the callback's parameter is itself declared by reference). array_map, array_filter, and array_column all return new arrays without touching the source.
After $b = array_filter([10 => 'x', 11 => 'y', 12 => ''], fn($v) => $v !== '');, what are the keys of $b? A. 0 and 1 (re-indexed) B. 10 and 11 (original keys preserved) C. 10, 11, and 12 D. It throws an error
B. 10 and 11 (original keys preserved) — array_filter() preserves the original keys of the elements it keeps rather than renumbering them — you'd need to wrap the result in array_values() to get a clean 0-based re-indexed array.
What's the key difference between array_slice() and array_splice()? A. They are aliases for the same function B. array_slice returns a portion without modifying the original; array_splice removes (and can replace) that portion in the original array itself C. array_slice mutates the array; array_splice does not D. array_splice only works on associative arrays
B. array_slice returns a portion without modifying the original; array_splice removes (and can replace) that portion in the original array itself — array_slice() is non-destructive — it reads and returns a window of the array, leaving the original untouched. array_splice() mutates the original array in place, removing the specified range and optionally inserting replacement elements there.
Given $employees = [['name'=>'Ada','dept'=>'Eng','salary'=>95000], ['name'=>'Grace','dept'=>'Eng','salary'=>110000], ['name'=>'Linus','dept'=>'Ops','salary'=>88000]];, write code that produces the average salary in the 'Eng' department using array_filter, array_column, and array_sum.
$engSalaries = array_column(array_filter($employees, fn($e) => $e['dept'] === 'Eng'), 'salary'); $average = array_sum($engSalaries) / count($engSalaries);
Filter to Eng-department employees first, then use array_column to pull just the salary field out of the filtered result, then array_sum divided by count.