Algorithms And Data Structures
Sorting And Comparator Correctness
Sorting is useful only when the comparator defines a consistent order and the chosen operation preserves the keys or stability the caller expects.
Why This Matters
Incorrect comparators produce unstable, environment-sensitive output and can break pagination, signatures, reports, and tests.
Working Model
A comparator should be antisymmetric and transitive, return zero for equivalent ordering keys, and apply deterministic tie-breakers when output must be stable.
Practical Rules
- Choose value sort versus key sort deliberately.
- Use the spaceship operator for clear comparisons.
- Add a unique tie-breaker for pagination.
- Normalize case and locale rules.
- Do not mutate source ordering unless intended.
Failure Modes
- Returning booleans from a comparator.
- Subtracting large values to compare them.
- Using locale-sensitive order without specifying locale.
- Sorting after paginating instead of before.
Verification
- Test equal primary keys.
- Test reverse and already-sorted inputs.
- Assert deterministic repeated results.
- Compare ordering with database queries.
What You Should Be Able To Do
After this lesson, you should be able to explain comparison sorting, PHP sorting APIs, and the invariants of a correct comparator, choose a suitable approach for a real PHP project, and verify the result instead of relying on assumptions.
Sorting Defines A Promise
Sorting is not just rearranging values until they look pleasant. It creates a promise that later code relies on. Pagination assumes a stable order. Reports assume rows are grouped and ranked consistently. Signatures and cache keys may assume a canonical order. Tests often assume repeated runs return the same sequence. When the comparator is vague or inconsistent, the sorted output becomes an unreliable foundation for everything that follows.
A comparator answers three questions for any two values: should the left value come before the right value, after it, or be considered equivalent for this ordering? In PHP, user comparison callbacks for functions such as usort() and uasort() should return an integer less than zero, zero, or greater than zero. Returning a boolean is a common bug because true and false do not express all three cases. Equality is a real result, especially when two rows share the same primary sort key.
The comparator must also be consistent. If A sorts before B, then B should not also sort before A. If A sorts before B and B sorts before C, then A should sort before C. If two values compare as equal, the caller needs to know whether their original order is meaningful, whether a later tie-breaker should decide, or whether either order is acceptable.
Choosing The Sorting API
PHP has several sorting functions because arrays can be lists or maps and because sometimes keys matter. sort() sorts values and reindexes numeric keys. rsort() does the same in reverse. asort() sorts values while preserving keys. arsort() preserves keys in reverse value order. ksort() and krsort() sort by key. usort(), uasort(), and uksort() use custom comparison callbacks for values, key-preserving values, and keys.
The first design decision is whether keys are part of the data contract. If a map is keyed by user ID, calling sort() destroys that relationship by reindexing the values. If the array is a display-only list, reindexing may be fine. The function name should match the shape: lists can usually use value sorts that reindex; maps usually need key-preserving sorts or a conversion to a list with an explicit name.
<?php
declare(strict_types=1);
$usersById = [
20 => ['name' => 'Grace'],
10 => ['name' => 'Ada'],
];
uasort($usersById, fn (array $a, array $b): int => $a['name'] <=> $b['name']);
echo implode(', ', array_keys($usersById)) . PHP_EOL;
// Prints:
// 10, 20
This example preserves the user IDs while sorting by name. If the code had used usort(), the output values might still look correct, but the keys would no longer be user IDs. That is the kind of bug that hides until later code performs a lookup.
The Spaceship Operator And Tie-Breakers
The spaceship operator, <=>, is usually the clearest way to write simple comparisons. It returns the negative, zero, or positive integer shape that PHP's sorting callbacks expect. It also makes chained tie-breakers readable.
<?php
declare(strict_types=1);
$rows = [
['id' => 3, 'score' => 90, 'name' => 'Linus'],
['id' => 1, 'score' => 95, 'name' => 'Ada'],
['id' => 2, 'score' => 95, 'name' => 'Grace'],
];
usort($rows, function (array $a, array $b): int {
return ($b['score'] <=> $a['score'])
?: ($a['name'] <=> $b['name'])
?: ($a['id'] <=> $b['id']);
});
echo $rows[0]['name'] . ', ' . $rows[1]['name'] . PHP_EOL;
// Prints:
// Ada, Grace
The first comparison sorts higher scores first. The second comparison sorts equal scores by name. The final comparison uses a unique identifier so the order is deterministic even when score and name match. That final tie-breaker is critical for pagination and repeatable reports. Without it, two equal rows may swap positions between requests, deployments, or database plans.
Avoid subtracting values as a comparison shortcut. It can be unclear for descending order, unsafe in fixed-width integer contexts, and wrong for non-integer values. The spaceship operator communicates intent and handles equality explicitly.
Normalization And Locale
Sorting strings requires a rule. Case-sensitive byte order, case-insensitive order, natural order, and locale-aware collation can all produce different results. A product list, an admin report, and a file manifest may need different rules. The lesson is not that one rule is always correct; the lesson is that the rule must be named and tested.
Normalize before sorting when the business rule demands it. If names should sort case-insensitively, compare normalized versions while preserving the original display value. If numeric strings should sort as numbers, validate and convert them before comparison. If a locale-sensitive order is required, the runtime environment and locale configuration become part of the behavior and need operational verification.
Database ordering must also match application ordering. A page of records sorted by SQL collation and then re-sorted in PHP can produce inconsistent pagination. Sorting after paginating is especially dangerous: the database returns page two according to one order, then PHP rearranges only those rows, not the whole result set. Sort before slicing, and prefer one authoritative ordering layer for paginated data.
Mutation And Copies
Most PHP sort functions mutate the array passed to them. That is fine when the sorted order is the new desired state. It is risky when the caller still needs the original order. If both orders matter, create a clearly named copy before sorting. Avoid sorting a shared array in a helper when the caller might assume it remains unchanged.
Mutation also affects review. A function named getSortedUsers() should not unexpectedly reorder a property on an object unless that side effect is the point of the method. A function named sortUsersInPlace() makes the side effect visible. The name should tell the caller whether ordering changes are local or shared.
Verification
Test equal primary keys because equality is where weak comparators fail. Test already-sorted and reverse-sorted input because they reveal assumptions about mutation and stability. Run the same sort more than once and confirm the same output when deterministic order is required. Compare PHP ordering with database ordering for features that cross that boundary.
Also test the keys. If the array is a map, assert that keys are preserved after sorting. If it is a list, assert that it remains a list after sorting. If pagination depends on the sort, test that adding a new row with the same primary key does not make existing rows jump unpredictably.
Stability, Pagination, And Review
Stability means that values considered equal by the comparator keep their relative order. Even when a particular sorting implementation behaves consistently in one runtime, application code should not depend on accidental stability unless the chosen API and versioned behavior explicitly promise it. The safer practice is to add a tie-breaker whenever the output leaves the current function. A unique database ID, creation timestamp plus ID, or original sequence number can turn a vague order into a repeatable one.
Pagination is where this becomes visible. Suppose an API sorts orders by created_at descending and many orders share the same timestamp. Page one can contain an order on the first request and page two can contain that same order on the next request if the database or PHP code is free to order ties differently. Users experience that as missing or duplicated rows. Adding id as the final tie-breaker gives every row a stable position.
Comparator review should include bad inputs as well as normal rows. Missing fields, null values, mixed integer and string data, and inconsistent casing should all follow a documented rule. If invalid data should be rejected before sorting, test that rejection separately. If invalid values sort last, encode that rule directly instead of relying on PHP's loose comparisons.
A good comparator is boring to read. It names the fields in priority order, handles equality, and avoids side effects. It should not query a database, mutate compared rows, read changing global state, or depend on the current time. Sorting calls the comparator many times, so hidden work inside the comparator can turn a simple ordering step into a slow and unpredictable operation.
When sorting crosses storage boundaries, keep one ordering authority. If SQL returns rows in one order and PHP changes that order afterward, pagination, cursors, and audit exports can drift. If PHP must apply a secondary order after loading data, document why the database cannot own that rule and test the full request path rather than the comparator alone.
After this lesson, you should be able to select the correct PHP sorting function, write a comparator that handles equality, add tie-breakers where deterministic order matters, preserve keys deliberately, and prove that the sorted output matches the same rule used by the rest of the application.
Practice
Practice: Sort Orders
Sort orders by newest creation time and then ascending ID.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Compare timestamps in descending order, then IDs ascending when equal. Keep the original list if callers still need it.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.
Practice: Fix A Boolean Comparator
Review a comparator that returns $a['price'] > $b['price'].
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Replace the boolean with a three-way comparison and add a deterministic tie-breaker. Test less-than, equal, and greater-than cases.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.
Practice: Align Database And PHP Order
Make PHP and SQL produce the same case-insensitive product ordering.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Define normalization or collation explicitly, use matching secondary keys, and test accented, mixed-case, and equal names rather than assuming defaults agree.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.