TomNomNom.com

Blogging since 2008 until 2010

Dynamic callbacks for PHP's usort

I recently wrote a blog post on a couple of PHP5.3's new features. As you may have noticed, I was really clutching at straws with my example use-cases. To make up for that, I have come up with a more realistic example of when you might want to use closures.

I seem to end up using multi-dimensional arrays quite often. Whatever the reason for that, sooner or later I need to sort them. In the past, I have just defined a callback function for use with usort(), but that has a lot of drawbacks.

If I am to successfully explain what the hell I'm on about, we will be needing some test data... Oh look! Here it comes now! Five different types of fruit in a multi-dimensional array.

<?php
$fruit = array(
  array('number' => 1'name' => 'apple',  'color' => 'red'),
  array('number' => 2'name' => 'orange''color' => 'orange'),
  array('number' => 3'name' => 'pear',   'color' => 'green'),
  array('number' => 4'name' => 'grape',  'color' => 'red'),
  array('number' => 5'name' => 'peach',  'color' => 'peach')
);

Come to think of it: we will be needing a nice way to tell people what fruit we have and in what order. Here's a dead simple function for doing just that:

<?php
function printItems($items){
  foreach ($items as $item){
    echo implode(', '$item) . "\\n";
  }
}

Let's give it a whirl!

<?php
printItems($fruit);
//Outputs:
//1, apple, red
//2, orange, orange
//3, pear, green
//4, grape, red
//5, peach, peach

Of course, when we print our original array it comes out in the order we defined it.

The old way

I'm a bit picky. I don't really want to tell people about my fruit in the order I thought of them. I'd much rather they were in alphabetical order. PHP doesn't have a native way to do that, so we need to define a sorting function and use usort() to do it.

<?php
function sortByName($a$b){
  return strCmp($a['name'], $b['name']);
}

The sorting function is used as a callback by usort(). It is given two elements from the array at a time; returns zero if they are the same, less than zero if the first one should come before the second, and more than zero if the second one should come before the first. Lucky for us, PHP's strCmp will do such a thing for us if we just want a string comparison; we just have to tell it which key we want to use.

<?php
usort($fruit'sortByName');
printItems($fruit);
//Outputs:
//1, apple, red
//4, grape, red
//2, orange, orange
//5, peach, peach
//3, pear, green

There is a problem with this however. If I wanted to then sort my fruit alphabetically by colour instead of by name, I would have to define a sortByColor() function. If I wanted to sort them by number, I would have to define a sortByNumber() function; and so on and so fifth.

What I need is a function that I can tell what I want to sort by. Before PHP5.3's introduction of closures that would have been possible, but tricky - maybe even ugly. PHP5.3 makes it trivial - maybe even elegant.

The easy way

Behold, the sortBy() function!

<?php
function sortBy(&$items$key){
  if (is_array($items)){
    return usort($itemsfunction($a$buse ($key){
      return strCmp($a[$key], $b[$key]);
    });
  }
  return false;
}

The input array $items is taken by reference and sorted with usort(). The callback function is created on the fly with a closure, useing the `$key[/key] provided.

Let's try it out!

<?php
sortBy($fruit'name');
printItems($fruit);
//Outputs:
//1, apple, red
//4, grape, red
//2, orange, orange
//5, peach, peach
//3, pear, green

sortBy($fruit'color');
printItems($fruit);
//Outputs:
//3, pear, green
//2, orange, orange
//5, peach, peach
//4, grape, red
//1, apple, red

It's alive! IT'S ALIIIVE! Well, it works anyway. There's still a little something missing though.

What if I want to sort my fruit in descending order instead of ascending? That's actually a pretty easy change. All I need to do is add an option to invert the output of the strCmp() function.

<?php
function sortBy(&$items$key$descending = false){
  if (is_array($items)){
    return usort($itemsfunction($a$buse ($key$descending){
      $cmp = strCmp($a[$key], $b[$key]);
      return $descending? -$cmp : $cmp;
    });
  }
  return false;
}

sortBy($fruit'number'true);
printItems($fruit);
//Outputs:
//3, pear, green
//5, peach, peach
//2, orange, orange
//4, grape, red
//1, apple, red

Magic! I can now sort by any key I want, in any direction I want. I've never been so happy.

Not quite...

Those of you have been paying attention may have noticed the fatal flaw in my use of strCmp(); I know - I'm one of the people who have noticed. If you want to sort by a numeric value, strCmp() would tell you that 20 comes before 3, and that 70 comes after 400 etc, because it does a string comparison.

This is a problem I intend to address in a blog post coming soon. Stay tuned!

First posted: Thu, 24 Sep 2009 15:41:28 +0000