natsort, Collator and the Spaceship: PHP Sorting Like a Human
Sort a file list with sort() and present it to a human, and the complaints write themselves: invoice-10.pdf sits before invoice-2.pdf, Zebra.txt beats apple.txt, and any name with an accent lands in the wrong neighborhood. Three separate problems, three built-in fixes.
Problem 1: embedded numbers - natural sorting
$files = ['img12.png', 'img2.png', 'img1.png', 'img10.png'];
sort($files); // img1, img10, img12, img2 - byte order
sort($files, SORT_NATURAL); // img1, img2, img10, img12 - human order
natcasesort($files); // same, case-insensitive, PRESERVES KEYS
SORT_NATURAL makes digit runs compare as numbers. The flag composes: SORT_NATURAL | SORT_FLAG_CASE for case-insensitive natural order in any flag-accepting sort. And when the values live in columns:
usort($files, fn ($a, $b) => strnatcasecmp($a['name'], $b['name']));
(The JS side of your app has the identical fix in Intl.Collator with numeric: true - use both and your listings agree everywhere.)
Problem 2: accents and locales - Collator
$collator = new Collator('de_DE');
$names = ['Zimmer', 'Öl', 'Anders', 'ähnlich'];
$collator->sort($names);
// Anders, ähnlich, Öl, Zimmer - umlauts sorted per German rules, case mixed correctly
$collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
$collator->sort($files); // natural + locale in one object
Collator (from the intl extension) is the grown-up answer: accent equivalence, case handling and per-language conventions (Swedish sorts ö after z on purpose; German doesn't) from CLDR data. For user-facing alphabetical lists in anything beyond ASCII English, it's the correct tool, full stop.
Problem 3: multi-key sorts - spaceship tuples
// Priority DESC, then created ASC, then natural name - one comparator
usort($tickets, fn ($a, $b) =>
[$b->priority, $a->createdAt, strtolower($a->name)]
<=> [$a->priority, $b->createdAt, strtolower($b->name)]
);
The array-vs-array spaceship compares element by element until one differs - multi-column ORDER BY in a single expression. Swap $a/$b per column for mixed directions, as with priority there. This idiom plus first-class callables for prebuilt comparators covers essentially every in-memory sort.
Cheat-sheet corner
- Keys matter? natsort/natcasesort/asort preserve them; sort/usort renumber. Sorting a keyed map with usort is the classic "where did my ids go" bug.
- Stability: PHP 8.0+ sorts are stable - equal elements keep their relative order, so chained "sort by X then by Y" via two passes actually works now.
- Database-side, the same humanity applies: natural order needs tricks (ORDER BY LENGTH(col), col is the cheap one); locale order is a collation setting. Sort where the pagination is - just sort correctly there.
Sorting is pure UX. The data doesn't care about order; the person scanning the list does - so borrow their rules.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.