Collection::sortBy
Orders a collection by a field, a computed value, or several fields at once.
Collection::sortBy(array|callable|string $callback, int $options = SORT_REGULAR, bool $descending = false): staticParameters
| Parameter | Type | Description |
|---|---|---|
$$callback | array|callable|string | A field name or dot path, a closure invoked as ($item, $key) returning the value to sort on, or an array of [field, direction] pairs and/or comparator closures for multi-attribute sorting. |
$$optionsoptional | int= SORT_REGULAR | A PHP sort flag such as SORT_NATURAL, SORT_STRING or SORT_NUMERIC applied to the comparison. |
$$descendingoptional | bool= false | Reverses the order. Ignored when $callback is an array of sort operations — put the direction in each pair instead. |
Return value
staticA new collection ordered by the resolved values, with the original keys preserved.
Changelog
- PHP 8.xAn array of [field, direction] sort operations may be passed to sort by several attributes at once.
- PHP 9.xThat array may also contain comparator closures of the form fn ($a, $b) => ..., which receive two whole items rather than one.
The workhorse for ordering rows and models. A string argument is resolved with data_get(), so dot paths reach into nested arrays and relations: sortBy('author.last_name'). A closure receives ($item, $key) and returns the value to sort on, which is how you order by something derived — a string length, a computed total, a status ranked against a fixed list.
Keys are preserved, as with every sort method here, so values() belongs at the end of the chain whenever the result is serialized or indexed by position.
Two different array forms
Passing an array switches to a completely separate code path. An array of [field, direction] pairs sorts by each in turn — [['name', 'asc'], ['age', 'desc']]. That path also accepts comparator closures taking ($a, $b), which is a different signature from the single-closure form's ($item, $key); mixing them up is a frequent bug. Critically, the multi-sort path ignores the $descending flag entirely, so direction must live inside each pair.
Sorting by a field some items lack is safe — data_get() yields null, which sorts before everything else under the default comparison.
Example
When to use it
- Order a table of products by price, then by name for ties.
- Sort loaded models by a related field such as author.last_name.
- Rank items by a status priority that is not alphabetical, using a closure and an index lookup.
Frequently asked questions
How do I sort by two fields at once?
Pass an array of [field, direction] pairs, such as sortBy([['name', 'asc'], ['age', 'desc']]). Each pair is applied in turn, and the third $descending argument is ignored on this path, so the direction must be inside the pairs.
Why does my closure get two items instead of an item and a key?
Because a closure inside the multi-sort array is a comparator receiving ($a, $b), while a closure passed on its own receives ($item, $key) and returns a sort value. The two forms are not interchangeable.
How do I sort text that ends in numbers correctly?
Pass SORT_NATURAL as the second argument, so 'Item 3' sorts before 'Item 12' instead of after it. Add SORT_FLAG_CASE to it for a case-insensitive natural sort.
More examples
Dot paths, closures and sort flags
Prints [Item 3, Item 1, Item 12] by author surname, then [Item 1, Item 12, Item 3] under plain string comparison, then [Item 1, Item 3, Item 12] with SORT_NATURAL. The last call sorts by title length, giving [Item 1, Item 3, Item 12] as well — the shorter titles come first.
<?php
$books = collect([
['title' => 'Item 12', 'author' => ['last' => 'Zhang']],
['title' => 'Item 3', 'author' => ['last' => 'Ali']],
['title' => 'Item 1', 'author' => ['last' => 'Moreau']],
]);
// data_get() means dot notation just works.
print_r($books->sortBy('author.last')->pluck('title')->all());
// Default string comparison puts 'Item 12' before 'Item 3'.
print_r($books->sortBy('title')->pluck('title')->all());
// SORT_NATURAL fixes it.
print_r($books->sortBy('title', SORT_NATURAL)->pluck('title')->all());
// A single closure receives ($item, $key) and returns a sort value.
print_r($books->sortBy(fn (array $b) => strlen($b['title']))->pluck('title')->all());Comparator closures in the multi-sort array
Prints Abigail 30, then Taylor 36, then Taylor 34. Direction for a comparator closure comes from swapping $a and $b, not from a 'desc' string — the multi-sort path calls the closure directly and never looks at a direction for it.
<?php
$people = collect([
['name' => 'Taylor', 'age' => 34],
['name' => 'Abigail', 'age' => 30],
['name' => 'Taylor', 'age' => 36],
]);
// Inside the array, closures take TWO ITEMS and return a comparison.
$sorted = $people->sortBy([
fn (array $a, array $b) => $a['name'] <=> $b['name'],
fn (array $a, array $b) => $b['age'] <=> $a['age'], // reversed = desc
]);
print_r($sorted->values()->all());Ranking by a non-alphabetical order
Prints [T-2, T-3, T-1]. Sorting on the severity string directly would give alphabetical order (critical, low, normal); mapping each value to its index in a fixed list is the standard way to impose a domain order. An unknown severity would return false, which sorts before index 0 — guard it if that matters.
<?php
$order = ['critical', 'high', 'normal', 'low'];
$tickets = collect([
['ref' => 'T-1', 'severity' => 'low'],
['ref' => 'T-2', 'severity' => 'critical'],
['ref' => 'T-3', 'severity' => 'normal'],
]);
$sorted = $tickets->sortBy(
fn (array $t) => array_search($t['severity'], $order, true)
);
print_r($sorted->pluck('ref')->all());
Discussion