php’s missing array_usearch

Pure geekery today, kids.

php has a bunch of functions that work on arrays. (In most modern languages arrays are defined by classes or prototypes and have methods built-in. php is not a modern language.) There are functions like array_sort that takes an array and puts the elements of that array in order. That’s fine if you have an array of numbers or strings, but for more complex things, how does the code decide which comes first?

For that use, there is another function, array_usort that takes an array, and you tell it what code to use to compare the two items.

php also has a method called array_search which finds whether an array has a particular item in it. As before this works fine for simple items, but becomes less useful as the items in the array grow in complexity, or you want to find something that you don’t already have a full example of. What if you have a list of books and you want to find the one titled Huckleberry Finn?

It seems logical that there would be a search function where, as for array_usort, you tell the code what defines a “match”, and then off it goes to see what comes up. Logical, but it’s not there (unless it’s tucked away with a terrible name that makes no sense, which is entirely possible in php).

So after about the eleventy-hundredth time writing a little loop to find something in an array I said, “dangit, I’m writing array_usearch.”

function array_usearch(array $array, Closure $test) {
    $found = false;
    $iterator = new ArrayIterator($array);
 
    while ($found === false && $iterator->valid()) {
        if ($test($iterator->current())) {
            $found = $iterator->key();
        }
        $iterator->next();
    }
 
    return $found;
}

All this does is try each element in the array against a function you provide until the function returns true, then it returns the key for that item in the array. If no match is found, it returns false, the same way array_search does. Simple! Using it would look something like this:

// define a type to put into a list
class Thing  {
    public $id;
    public $name;
 
    public function __construct($id, $name, $category) {
        $this->id = $id;
        $this->name = $name;
    }
}
 
// make a list of them, mixed up a bit
$listOfThings = [
    new Thing(1, 'one'),
    new Thing(2, 'two'),
    new Thing(4, 'four'),
    new Thing(3, 'three'),
];
 
// find the index of the item with id = 4
$id4Index = array_usearch($listOfThings, function($thing) {
    return $thing->id === 4;
});
// $id4Index will now be 2

The function will work on all php array types, whether with numeric indices or strings.

php purists might object to using the name array_usearch because all the other array_u* functions take a callable for defining the function, while this version uses a Closure. There are a couple of reasons: 1) Closures didn’t exist in php when the array_u* functions were defined, 2) it’s the 21st century now and other languages use closures in this manner for a reason, and 3) closures allow the function that gets passed to array_usearch to be reused with different values. With a little extra setup we can make searching super-clean:

// function that returns an anonymous function that captures the id to search for
$idClosure = function($id) {
    return function($item) use ($id) {
        return $item->id = $id;
    }
}
 
$id4Index = array_usearch($idClosure(4)); // value will be 2
$id2Index = array_usearch($idClosure(2)); // value will be 1

Now we can write code compactly that can search for matches of arbitrary complexity, and we can create little factories to produce the search functions themselves, so the complexity is tucked away out of sight. This variation takes an array of key/value pairs and searches for items that match all of those values:

function firstIndexMatching(array $array, array $criteria, bool $useStrict = true) {
 
    if (count($criteria) < 1) {
        return false;
    }
 
    // create a closure that has captured the search criteria
    $testWithCriteria = function($criteria, $useStrict) {
 
        return function($item) use ($criteria, $useStrict) {
 
            foreach($criteria as $key => $value) {
                if (!isset($item->$key)) {
                    return false;
                } else if ($useStrict && $item->$key !== $value) {
                    return false;
                } else if (!$useStrict && $item->$key != $value) {
                    return false;
                }
            }
 
            return true;
        };
    };
 
    return array_usearch($array, $testWithCriteria($criteria, $useStrict));
}

Now if you have an array of people, for instance, you can search for the first match with a given name:

$joeCoolIndex = firstIndexMatching($people, [
    'firstName' => 'Joe',
    'lastName' => 'Cool'
]);

The loop and the comparisons are moved out of the way and all the main part of your code need to do is supply the criteria for the search.

Ultimately after a search like this, you will want to have the item, not just its index. That’s easy enough, but don’t forget that if no match is found, array_usearch will return false, which php will often conflate with 0, so extra care has to be taken when using the returned index.

$joeCool = $joeCoolIndex !== false ? $people[$joeCoolIndex] ?? null : null;

Obviously this could be added to the firstIndexMatching function if one is never interested in the index itself.

And there you have it! A simple callback-based search function, ready to keep your main code clean and clear.

2

2 thoughts on “php’s missing array_usearch

  1. More than once I have “created” a function in PHP, only to discover it already exists under some surprising name. I don’t know if it’s a deficiency in the language, the documentation, or the user.

    • Mostly the first two, I think. There are massive parts of the language that remain undocumented after several years (DOM springs to mind), and because object-orientation is an afterthought, you can’t just look at the surface of a class to see how to deal with it. There’s a globally-defined function out there to do what you want, but you have to know its name first. Makes me want fo define a function named rumplestiltskin.

Leave a Reply

Your email address will not be published. Required fields are marked *