Collection::sortBy

Orders a collection by a field, a computed value, or several fields at once.

Signature
Collection::sortBy(array|callable|string $callback, int $options = SORT_REGULAR, bool $descending = false): static

Parameters

ParameterTypeDescription
$$callbackarray|callable|stringA 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.
$$optionsoptionalint= SORT_REGULARA PHP sort flag such as SORT_NATURAL, SORT_STRING or SORT_NUMERIC applied to the comparison.
$$descendingoptionalbool= falseReverses 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.