PHP from the ground up

Array functions instead of loops

Map, filter and sort: the operations that loops were doing, said by name — and the arrow functions that describe each one in a line.

Part 8 of 13

Intermediate16 min readBack to Courses

The three questions every loop is answering

Most loops over an array are doing one of three things: turning each item into something else, keeping only the items that pass a test, or folding the whole list into one value. PHP names all three: array_map, array_filter, array_reduce — and the three say what they mean, where a foreach makes a reader reconstruct the intention from the body.

Each takes a function and hands it one item. The short spelling of that function is the arrow function: fn ($name) => strtoupper($name) — an expression only, no return needed, and the outer variables are available automatically. It is the same => as in the keyed arrays, doing the same job: naming a relationship.

Not every loop should become a chain. When the body does two or three different things — build a line, add a total, remember the best — a foreach is clearer than three passes over the same list. The array functions are at their best when each step has one job and the steps can be read in order.

Chains, read left to right

A chain of array functions is read from the inside out, which is the opposite of how it looks. implode(", ", array_column(array_filter($orders, $test), "item")) starts with the filter, passes the survivors to array_column, and joins the result. Reading it aloud from the innermost call outwards is the way to check it, and assigning each step to a variable is the way to debug it.

There is no performance prize for the chain, and no penalty worth worrying about at ordinary sizes. The reason to write it is that the intention is visible: filter, then take the column, then join. A loop with the same three steps is not wrong; it just makes the reader assemble the intention from the body.

Sorting, and the spaceship

Plain sort($list) sorts by value and throws the keys away; asort keeps them. Keyed rows are sorted with usort($rows, $comparator), where the comparator is a function of two items saying which comes first — and the odd-looking <=> operator is exactly that answer: negative when the left one sorts first, positive when the right one does, zero when they tie. fn ($a, $b) => $b['score'] <=> $a['score'] sorts by score, highest first, because the operands are the other way round.

Sorting is in place: usort($rows, $cmp) changes $rows and returns only a success flag. Keeping the original around needs a copy, via $sorted = $rows; before the call.

Joining, splitting, fetching a column

implode(', ', $names) turns a list into one string, explode(',', $text) turns a string back into a list, and the pair is how text and arrays meet. array_column($rows, 'name') reaches into a table of records and pulls out one field as a plain list — the shortest step from rows to names, and the input most other functions want.

Composition is the point: implode(', ', array_column(array_filter($orders, fn ($o) => $o['total'] > 50), 'item')) reads left to right as "the items of the orders over fifty, joined". Each piece does one thing, which is why the line, though long, can be checked a piece at a time.

Map, filter, sort, join

<?php
$prices = [12.5, 4.0, 27.9, 8.25];

// Each price with tax: one list becomes another, same length.
$withTax = array_map(fn ($price) => round($price * 1.2, 2), $prices);
echo implode(', ', $withTax) . PHP_EOL;

// The affordable ones: a shorter list, same order.
$cheap = array_filter($prices, fn ($price) => $price < 20);
echo implode(', ', $cheap) . PHP_EOL;

// One number from many: reduce (sum is its common case).
$total = array_reduce($prices, fn ($sum, $price) => $sum + $price, 0);
echo 'Total: ' . $total . PHP_EOL;

// A string back into a list.
$tags = explode(',', 'php,arrays,functions');
echo count($tags) . ' tags' . PHP_EOL;

// Rows sorted by one field, highest first.
$rows = [
    ['name' => 'lamp',  'score' => 71],
    ['name' => 'chair', 'score' => 93],
    ['name' => 'desk',  'score' => 88],
];
usort($rows, fn ($a, $b) => $b['score'] <=> $a['score']);
foreach ($rows as $row) {
    echo str_pad($row['name'], 8) . $row['score'] . PHP_EOL;
}

A results board

Run it and follow each operation. Then add a filter that keeps only scores above 80, count them, and join their names into one line with implode.

Output
Click "Run" to execute your code

array_filter keeps the original keys, so the result can have gaps — 0, 2, 5. That matters for output only; when the numbering should be fresh, wrap the result in array_values(...).

Callbacks, keys and copies

Each of these functions takes a callable and decides when to call it. An arrow function fn ($x) => $x * 2 is an expression, and it captures variables from the surrounding scope by value automatically — no use clause, and no surprise when the outer variable changes later. That is enough for the one-line tests these functions want.

array_map keeps the keys of a single input; array_filter keeps the keys of whatever survives, so the result can have gaps. array_values() renumbers a result when the gaps are not wanted. The sort functions are different: usort, asort and their relatives change the array in place and return a boolean, so $sorted = usort($rows, $cmp) is a mistake — copy first when the original order still matters.

Three array-function mistakes are worth naming: indexing a filtered result at [0] when the first surviving key was 3, expecting usort() to return the sorted array, and calling array_filter() without a callback, which quietly removes every falsy value. Reindex with array_values(), keep the return value of a sort out of an assignment, and write the test you mean.

Try these next

  • Filter a list of prices to those under 20 and reindex the result with array_values().
  • Sort three rows by name with usort() and print the original array afterwards to prove it changed.
  • Reduce a list of totals to one sum, starting the accumulator at 0.0.

An order queue

Run the queue, then add a fifth order and change one paid flag. Print the unpaid count as well as the names, and keep the largest-first order for the table.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Which pair describes array_map and array_filter?