php: powerful sorting with usort

i recently worked on a rescue project where the original dev wanted to sort an array of item objects first by manufacturer name and then by price, descending. their ‘solution’ was an eighty-line foreach mess of fiddling with array keys. it was difficult to read, more difficult to modify, and didn’t even work correctly lots of the time. i replaced the whole thing with a five-line usort call.

usort stands for ‘user-defined sort’. it allows us, as devs, to write our own sorting rules (plural!) and makes working with arrays of complex data easier.

in this post, we’re going to take a thorough look at sorting arrays with usort. we’ll go over the basics of how the function works, write some ascending and descending sorts on integers and strings, and then cover how to sort on multiple values.

best other sorting functions can do for you is one rule on an array of primitives

our sample array

all the examples we’ll be looking at will be using an array of objects that represents a small, but well-curated, collection of vinyl record albums. it looks like this.

$albums = [
    (object)[
        'artist' => 'Bratmobile',
        'title' => 'Pottymouth',
        'year' => 1993,
    ],
    (object)[
        'artist' => 'Monk, Thelonious',
        'title' => 'Brilliant Corners',
        'year' => 1957,
    ],
    (object)[
        'artist' => 'Fugazi',
        'title' => 'In on the kill taker',
        'year' => 1993,
    ],
    (object)[
        'artist' => 'Monk, Thelonious',
        'title' => '5 by Monk by 5',
        'year' => 1959,
    ],
];

the basics of usort

usort takes two arguments: the array that we want to sort, and a callable function that defines the rule(s) we want to use for sorting.

the array argument is passed by reference. this means that the array is modified in place. usort does not return a new, sorted, array. rather, the original array is sorted.

the callable argument is a function that, itself, takes two arguments. these two arguments represent two arbitrary elements of the array we want to sort, and the return value of the callable is an integer that indicates which of those two arguments should be considered ‘higher’ in the sort order.

this integer must be one of:

  • 0 if the two arguments are equal
  • 1 if the left argument is greater than the one on the right
  • -1 if the right argument is greater

we can do any comparison we want in the body of callable. this is the power of usort.

let’s look at some examples.

basic sorting: albums by year

let’s start with doing some straightforward sorting on a nice, clean integer value: the year our albums were released.

here’s what that would look like:

// sorting callable
$sortByYearAsc = function($a, $b) {
    if($a->year == $b->year) {
        return 0;
    }

    if($a->year > $b->year) {
        return 1;
    }

    if($a->year < $b->year) {
        return -1;
    }
};

// calling usort
usort($albums, $sortByYearAsc);

the thing to pay attention to here is how we’ve constructed our callable.

we’ve created an anonymous function and assigned it to a variable. our function accepts two arguments, representing two arbitrary elements: the individual album objects in the array we’re sorting.

in the body of the function, we do our comparison testing on the year element of the album objects. if the year of the left argument is higher, we return 1. if the right argument is higher, -1, and if the two years are equal, we return 0.

if we print_r() our sorted array it will be sorted by year, exactly as we expect:

Array
(
    [0] => stdClass Object
        (
            [artist] => Monk, Thelonious
            [title] => Brilliant Corners
            [year] => 1957
        )

    [1] => stdClass Object
        (
            [artist] => Monk, Thelonious
            [title] => 5 by Monk by 5
            [year] => 1959
        )

    [2] => stdClass Object
        (
            [artist] => Bratmobile
            [title] => Pottymouth
            [year] => 1993
        )

    [3] => stdClass Object
        (
            [artist] => Fugazi
            [title] => In on the kill taker
            [year] => 1993
        )

)

ascending and descending sorts

our current callable sorts our albums by year ascending, ie. oldest first.

if we want to change to a descending order — put the newest albums at the top — all we have to do is reverse our comparisons in our callable:

$sortByYearDesc = function($a, $b) {
    if($a->year == $b->year) {
        return 0;
    }

    if($a->year > $b->year) {
        return -1;
    }

    if($a->year < $b->year) {
        return 1;
    }
};

here, we’ve changed it so that instead of 1 being returned if the left year is greater and -1 if the right year is greater, the inverse is true. we’ve basically just switched the 1 and -1.

the result is that the comparisons still work, just in reverse, and we get our descending sort.

enter the ‘spaceship operator’

our sorting callables work, but they are ugly. three if statements in a row? that’s code smell.

sure, we could tighten them up with an else if and a default return value, but there’s a much cleaner solution: the spaceship operator

let’s look at an example:

$a <=> $b;

the spaceship operator itself is the <=>. if you squint, it kind of looks like a crude ascii art of a spaceship. hence the name.

when we compare two values with the spaceship operator we get zero if both sides are equal, 1 if the left side is greater, and -1 if the right side is greater. this is exactly the sort of comparison we want to do for usort()!

let’s rewrite our callable to use the spaceship operator:

$sortByYearAsc = function($a, $b) {
    return $a->year <=> $b->year;
};

this does everything that our ugly, three-if statement function did and is much cleaner and terser.

if we want to, we can tighten this up even more by rewriting it as an arrow function

$sortByYearAdc = fn($a, $b) => $a->year <=> $b->year;

and, if we want to change the sort order from ascending to descending, all we need to do is switch the operands: swap $a and $b.

sorting on strings

nobody sorts their album collection by release year. what we probably want to do is sort alphabetically by artist.

fortunately, php has some built-in functions for doing string comparisons:

  • strcmp compares two strings
  • strcasecmp compares two strings without case sensitivity

both these functions take two arguments: the strings to compare. and both behave like the spaceship operator, returning 1 if the left argument is greater, -1 if the right argument is greater, and 0 if the arguments are the same.

let’s look at how we would write a callable to sort on artist using strcasecmp:

$sortByArtistAsc = fn($a, $b) => strcasecmp($a->artist, $b->artist);

usort($albums, $sortByArtistAscSpaceship);

in our callable here we have replaced our spaceship operator comparing years with a call to strcasecmp to compare artists. the results are precisely what we want:

Array
(
    [0] => stdClass Object
        (
            [artist] => Bratmobile
            [title] => Pottymouth
            [year] => 1993
        )

    [1] => stdClass Object
        (
            [artist] => Fugazi
            [title] => In on the kill taker
            [year] => 1993
        )

    [2] => stdClass Object
        (
            [artist] => Monk, Thelonious
            [title] => 5 by Monk by 5
            [year] => 1959
        )

    [3] => stdClass Object
        (
            [artist] => Monk, Thelonious
            [title] => Brilliant Corners
            [year] => 1957
        )

)

sorting on multiple elements

people who really care about how their albums are organized may argue about a lot of things (is “iggy pop” filed under “i” or “p”? we have opinions), but there is one thing everyone agrees on: albums are filed alphabetically by artist, and the albums of each artist are filed in chronological order of release.

this means that to properly sort our array of albums, we need to sort by artist first, and then by year. we’re looking here to replicate the functionality of sql’s

ORDER BY artist ASC, year ASC

this is precisely the sort of thing that usort excels at. let’s look at how we might do that:

$sortByArtistThenYear = function($a, $b) {
    if(strcasecmp($a->artist, $b->artist) == 0) {
        return $a->year <=> $b->year;
    }

    return strcasecmp($a->artist, $b->artist);
};

usort($albums, $sortByArtistThenYear);

here, we’re just taking the things we’ve already covered — string comparison with strcasecmp and integer comparison with the spaceship operator — and combining them.

first, we see if the two artist fields are the same by testing them with strcasecmp. if they are, we know that we need to sort the albums for that artist by year. we do that with the spaceship operator.

if the artist fields are not the same, we do our standard sorting on the artist field.

the result is that our array is sorted ascending by artist and, if there is more than one record with the same artist, those records are sorted ascending by year. it looks like this:

Array
(
    [0] => stdClass Object
        (
            [artist] => Bratmobile
            [title] => Pottymouth
            [year] => 1993
        )

    [1] => stdClass Object
        (
            [artist] => Fugazi
            [title] => In on the kill taker
            [year] => 1993
        )

    [2] => stdClass Object
        (
            [artist] => Monk, Thelonious
            [title] => Brilliant Corners
            [year] => 1957
        )

    [3] => stdClass Object
        (
            [artist] => Monk, Thelonious
            [title] => 5 by Monk by 5
            [year] => 1959
        )

)

conclusion

usort is an incredibly powerful tool and once we understand how to write sort rules for it we can order arrays of complex data any way we want to.

Posted by: grant horwood

co-founder of fruitbat studios. cli-first linux snob, metric evangelist, unrepentant longhair. all the music i like is objectively horrible. he/him.

Leave a Reply