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 toint, 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_NATURALfor numbers in text.Collatorfor umlauts —sort()putsÄafterZ.uasort()instead ofusort()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, ...).