In PHP, how do you write a usort() comparator with the <=> operator, and when do you switch to uasort() or uksort()?
answer
- negative, zero or positive int
- swap operands for descending
- arrays on both sides for tie-breakers
- uasort keeps keys, uksort compares keys
- bool return deprecated since 8.0
basics
~20 sA usort() comparator takes two elements and returns an int below, at or above zero; $a <=> $b does that in one expression. usort() renumbers keys, uasort() keeps them, and uksort() compares the keys instead.
solid answer
~40 sThe callback receives two elements and must return an **integer**: negative if the first should come first, zero if they tie, positive otherwise. The spaceship operator `<=>` returns `-1`, `0` or `1`, so `fn($a, $b) => $a <=> $b` is ascending and `$b <=> $a` is descending. For several sort keys, compare arrays: `[$b['score'], $a['seconds']] <=> [$a['score'], $b['seconds']]` sorts by score descending, then by time ascending. `usort()` renumbers the keys, `uasort()` keeps each key with its element (a leaderboard keyed by player id), and `uksort()` passes the keys to the comparator. Returning a bool such as `$a > $b` has been deprecated since PHP 8.0, and returning a float difference is cast to int, so `0.4` counts as a tie.
code
php · 14 lines<?php
declare(strict_types=1);
$entries = [
'p17' => ['score' => 820, 'seconds' => 41],
'p03' => ['score' => 950, 'seconds' => 58],
'p42' => ['score' => 820, 'seconds' => 37],
];
// Score high to low, then faster time first; player ids stay as keys
uasort($entries, fn(array $a, array $b): int =>
[$b['score'], $a['seconds']] <=> [$a['score'], $b['seconds']]);
echo implode(',', array_keys($entries)), PHP_EOL; // p03,p42,p17go deeper
Recall the contract: return a negative, zero or positive int, and <=> produces exactly that. Swap $a and $b to sort descending.
Explain multi-key sorting with arrays compared by <=>, the choice between usort, uasort and uksort, and why bool and float-difference comparators misbehave.
Review comparators for consistency, hidden key loss and deprecated bool returns; know that a strict_types return type turns a float difference into a TypeError rather than a silent tie.
Push sort rules into small named comparator functions that can be tested, so ranking logic such as tie-breaks is explicit and shared instead of repeated as inline closures.
## The comparator contract `usort()`, `uasort()` and `uksort()` let you define the order yourself. You pass the array and a **comparator**: a callable that receives two elements, `$a` and `$b`, and returns an integer. - **Negative**: `$a` belongs before `$b`. - **Zero**: they are equal for sorting purposes (a tie). - **Positive**: `$a` belongs after `$b`. Only the sign matters. PHP calls the comparator many times while it sorts, so it must also be **consistent**: the same two inputs always give the same sign, and if `a < b` and `b < c` then `a < c`. A comparator that breaks this (random results, or one that depends on external state that changes mid-sort) produces an order you cannot predict. ## Why `<=>` fits The spaceship operator `<=>` compares two values and returns `-1`, `0` or `1`, which is exactly the contract. That turns most comparators into one arrow function: - Ascending by value: `fn($a, $b) => $a <=> $b` - Descending: swap the operands, `fn($a, $b) => $b <=> $a` - By a field: `fn(array $a, array $b) => $a['score'] <=> $b['score']` There is no `rusort()`; direction always lives in the comparator. ## Several sort keys in one comparison When `<=>` compares two arrays of the same size and keys, it walks them element by element and returns the first non-zero result. That makes a **multi-key** comparator a single expression. For a quiz leaderboard where a higher score wins and, on equal scores, the faster player wins: ```php fn(array $a, array $b): int => [$b['score'], $a['seconds']] <=> [$a['score'], $b['seconds']] ``` The first slot has `$b` on the left, so score is descending; the second slot has `$a` on the left, so time is ascending. Mixing the operands per slot is how you set a direction per key. ## usort, uasort or uksort | Function | Comparator receives | Keys after the sort | |---|---|---| | `usort()` | the values | renumbered `0..n-1` | | `uasort()` | the values | kept with their values | | `uksort()` | the keys | kept with their values | A list of row arrays that you will render in order suits `usort()`. A leaderboard stored as `playerId => row` needs `uasort()`, otherwise the ids vanish. `uksort()` is for ordering by the key itself with rules no sort flag expresses, such as tier keys `gold`, `silver`, `bronze` ranked through a lookup table. ## Comparators that look right and are not 1. **Returning a bool.** `fn($a, $b) => $a > $b` returns `true` or `false`. Since PHP 8.0 this raises `E_DEPRECATED` ("Returning bool from comparison function is deprecated, return an integer less than, equal to, or greater than zero"). PHP 8.5 still copes by calling the comparator again with the operands swapped when it gets `false`, but the code is on a deprecation path and should return `<=>` instead. 2. **Subtracting floats.** `fn($a, $b) => $a - $b` works for integers but not for floats: a non-integer return value is cast to `int`, so a difference of `0.4` becomes `0` and two different averages are treated as equal. Use `<=>`, which compares the floats directly. 3. **Losing the keys.** Using `usort()` where the keys are identifiers drops them without any warning. The fix is `uasort()`. 4. **Declaring the wrong return type.** In a `declare(strict_types=1)` file, `fn(float $a, float $b): int => $a - $b` fails with a `TypeError`, because the difference of two floats is a float and strict mode will not convert it to `int`. That is louder than the silent cast, but it is still the wrong comparator. ## Keep the comparator cheap A sort calls the comparator many times, roughly `n log n` times for `n` elements, so work inside it is multiplied: - **Precompute derived keys.** If you sort by a lowercased name or a parsed date, compute that value once per element before sorting (for example, into a field of each row) instead of inside every comparison. - **Avoid I/O and queries.** A comparator that looks something up in a database or a file turns one sort into thousands of calls. - **Type the parameters.** `fn(array $a, array $b): int` or `fn(Player $a, Player $b): int` documents what is being compared and fails fast if the array holds something unexpected. ## Putting it together A good comparator is short, pure and returns `<=>`. Ties are handled by adding a second slot to the compared arrays, direction by which operand goes on the left, and key handling by choosing between `usort()`, `uasort()` and `uksort()` rather than by anything inside the callback.
- Why does comparing two arrays with <=> give a multi-key sort?When both arrays have the same size and keys, `<=>` compares them element by element in order and returns the first non-zero result. So `[$a1, $a2] <=> [$b1, $b2]` looks at the second slot only when the first ties. Putting `$b` on the left in a slot makes that key descending.
- What goes wrong if a comparator uses random_int() or reads a value that changes during the sort?The sort assumes the comparator is consistent: the same pair always gives the same sign, and the ordering is transitive. If it is not, PHP still finishes, but the resulting order is unpredictable and can differ between runs. Shuffling belongs to `shuffle()`, not to a comparator.
- When is uksort() the right choice over ksort()?When the key order needs rules the flags cannot express. `ksort()` with a flag handles numeric, string or natural order; `uksort()` lets you, for example, order tier keys such as `gold`, `silver` and `bronze` by a lookup table of ranks, while still keeping every key with its value.
saying these in an interview costs you the question
- A comparator should return true when $a is greater than $b
- usort() keeps the array's original keys
- Returning $a - $b is safe for floats as well as ints
- Descending order needs a separate rusort() function
- uksort() passes the values to the comparator