A queue sorted by priority often needs equal-priority records to retain their arrival order. Another report may need a reproducible identifier order instead. Those are different requirements, and a comparator should make the intended behavior clear. This example also checks whether sorting preserves the array's original keys.

Prerequisites: PHP 8.3 CLI or another supported PHP 8 release. The executable example was tested on PHP 8.3.6 and needs no extensions or packages beyond the standard runtime.

The official usort documentation states that equal elements retain their original order starting with PHP 8.0. Older PHP code should be reviewed during migration because that relative order was previously undefined. Use a supported runtime rather than maintaining a dependency on undefined tie behavior.

Run three intentional sorting policies

Save the following as stable.php. Ticket C arrives before ticket B, and both have priority two.

<?php
declare(strict_types=1);
function check(bool $condition): void {
    if (!$condition) throw new RuntimeException('check failed');
}
$rows = [
    'ticket-c' => ['id' => 'C', 'priority' => 2],
    'ticket-a' => ['id' => 'A', 'priority' => 1],
    'ticket-b' => ['id' => 'B', 'priority' => 2],
];
$original = $rows;
usort($rows, fn(array $a, array $b): int => $a['priority'] <=> $b['priority']);
check(array_column($rows, 'id') === ['A', 'C', 'B']);
check(array_keys($rows) === [0, 1, 2]);
$kept = $original;
uasort($kept, fn(array $a, array $b): int => $a['priority'] <=> $b['priority']);
check(array_keys($kept) === ['ticket-a', 'ticket-c', 'ticket-b']);
$explicit = $original;
usort($explicit, fn(array $a, array $b): int =>
    ($a['priority'] <=> $b['priority']) ?: strcmp($a['id'], $b['id']));
check(array_column($explicit, 'id') === ['A', 'B', 'C']);
$empty = [];
usort($empty, fn($a, $b): int => $a <=> $b);
check($empty === []);
echo 'stable: ', implode(',', array_column($rows, 'id')), PHP_EOL;
echo 'explicit: ', implode(',', array_column($explicit, 'id')), PHP_EOL;
echo "key and empty checks passed\n";

Execute the example

php stable.php

Expected output:

stable: A,C,B
explicit: A,B,C
key and empty checks passed

The first comparator returns zero for C versus B, preserving their input order. The second sort applies the same comparison while retaining associations between records and their original keys. The uasort documentation describes that key-preserving behavior.

The third comparator adds the identifier as a secondary key. It intentionally changes the tie policy: B now precedes C. Use this form when independently produced input arrays must converge on the same ordering, provided identifiers are unique and their bytewise order is the desired rule.

Edge cases and troubleshooting

A stable sort only preserves the order it receives. If a database query supplies equal-priority rows without a defined order, stability cannot reconstruct arrival order. Establish that upstream order or include a sequence number in the comparison.

Comparators must return a negative integer, zero or a positive integer. A Boolean comparison cannot correctly represent all three relationships. Returning a fractional difference can also collapse distinct values when PHP converts the result to an integer. Here priorities are integers, so the spaceship operator expresses the comparison directly.

Validate record fields before sorting if data comes from outside the application. Missing priorities and mixed data types are not handled by this fixture. Keep comparators free of mutations and external state. The custom check function throws on failure regardless of PHP's assertion configuration, and the empty-array check confirms that the example also handles an empty report.

Read more PHP tutorials.