PHP: How do you sort an array by a column?

Sorting records by a column is one line. The older variant with the minus sign, which you still find in many answers, fails on exactly the data type people sort most often: prices.

The answer

<?php
  usort($servers, fn($a, $b) => $a['price'] <=> $b['price']);
?>
ascending:  VPS 200/4.99  VPS 500/4.99  RS 2000/12.99  RS 4000/29.99

To sort descending, swap the two sides:

<?php
  usort($servers, fn($a, $b) => $b['price'] <=> $a['price']);
?>

<=> is called the spaceship operator and returns -1, 0 or 1. Exactly what usort() expects.

Why not with minus

Older answers often have this instead of <=>:

<?php
  usort($servers, fn($a, $b) => $a['price'] - $b['price']);   // broken
?>

The reason usually given for why it is bad is “integer overflow”. That rarely happens in practice, though. The case you actually meet is a different one:

$x['price'] - $y['price'] : -0.5
cast to int               : 0   <- 0 means "equal"!
with <=>                  : -1

PHP casts the return value of the comparison callback to int. -0.5 becomes 0 in the process, and 0 tells usort() that the two elements are equivalent.

Which means: every price difference under one unit disappears. 4.99 and 5.49 count as equal. With a price list roughly half of all comparisons are broken, and the result still looks plausible at first glance — it is roughly sorted, after all.

With whole numbers the minus variant works. But there is no reason to use it now that <=> exists.

Sorting by two columns

When two records share the same price, you often want a second criterion. The obvious solution is a cascade of ifs. It can be shorter:

<?php
  usort($servers, fn($a, $b) =>
      [$a['price'], $a['name']] <=> [$b['price'], $b['name']]);
?>
price, then name: VPS 200/4.99  VPS 500/4.99  RS 2000/12.99  RS 4000/29.99

<=> compares arrays element by element and stops at the first difference. That is exactly “by price, by name on a tie” and reads better than three nested ifs.

Sorting has been stable since PHP 8.0

A detail worth knowing, because it affects old code:

equal prices: erster/5  zweiter/5  dritter/5

Elements with the same sort value keep their original order. That has been guaranteed since PHP 8.0. Before that it was not — the order depended on the implementation and could change between PHP versions.

In practice this means: if you sort by price and the list was already sorted by name, afterwards it is sorted “by price, by name on a tie”. Without you having to write the second criterion.

I would still only rely on that when both sorts sit right next to each other. Otherwise somebody adds a line in between later and is left wondering.

array_multisort() and array_column()

The second widespread variant:

<?php
  array_multisort(array_column($servers, 'price'), SORT_ASC, $servers);
?>

It works and it is short. Two things bother me about it: the array is restructured by reference, and with several criteria the call quickly becomes unreadable. I stick with usort().

array_column() on its own, on the other hand, is very useful:

array_column($servers, 'price')         : [12.99,4.99,29.99,4.99]
array_column($servers, 'price', 'name') : {"RS 2000":12.99,"VPS 200":4.99,...}
array_column($servers, null, 'name')    : keys are now the names

The last variant — null as the second parameter — re-keys whole records by a column. You need that constantly when matching two result sets against each other, and it is considerably faster than a foreach loop.

Sorting strings: two surprises

First, the business with numbers in text:

sort()       : ["Item2","item10","item100","item9"]
SORT_NATURAL : ["Item2","item9","item10","item100"]

sort() compares character by character, and 1 comes before 9. That is why item100 ends up before item9. With SORT_NATURAL (or natsort()) the number is read as a number. For file names, version numbers and anything that is numbered through, that is what you want.

Second — and this is the point I actually want to make here — the umlauts:

sort()          : ["Apfel","Baum","Zebra","Ähre"]
Collator de_DE  : ["Ähre","Apfel","Baum","Zebra"]

Ähre ends up after Zebra. The reason is that sort() compares bytes. In UTF-8 Ä is the byte sequence 0xC3 0x84, and 0xC3 is greater than the 0x5A of Z. So every umlaut, every accent, everything outside ASCII lands at the back.

For a list a human reads — names, places, products — you therefore need Collator from the intl extension:

<?php
  $col = new Collator('de_DE');
  $col->sort($names);
?>

That sorts by the rules of the respective language, and those are not the same everywhere: in German Ä is filed under A, in Swedish it sits right at the end of the alphabet.

You can check whether intl is available with class_exists('Collator'). Most hosts ship the extension, but it is not guaranteed.

usort() throws the keys away

Finally a detail that bites with associative arrays:

usort  keys : [0,1,2]         <- the keys are gone
uasort keys : ["b","c","a"]   <- preserved

usort() reassigns the keys. If your records are keyed by ID or name, you need uasort(). And if you want to sort by the keys themselves, uksort().

The mnemonic: a stands for “associative” (keys are kept), k for “key” (sort by keys).

Summary

  • usort($data, fn($a, $b) => $a['column'] <=> $b['column']).
  • Never with -: the return value is cast to int, so every difference under 1 becomes “equal”.
  • Two criteria: [$a['x'], $a['y']] <=> [$b['x'], $b['y']].
  • Sorting has been stable since PHP 8.0.
  • array_column($data, null, 'column') for re-keying.
  • SORT_NATURAL for numbers in text.
  • Collator for umlauts — sort() puts Ä after Z.
  • uasort() instead of usort() when the keys should be preserved.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).