Mapping, reducing and filtering in PHP
Juan Miguel Medina Prieto
Posted on January 3, 2020
There's this thing called "functional programming" that you might have heard about, particularly if you have been learning about JavaScript and let's not mention if you have used languages like Scala or Haskell. It's obviously not as popular as object-oriented programming and using it will likely have its pros and cons, just as any programming paradigm or any programming language. Unfortunately, I can't say I'm experienced enough to compare both of those without ending up essentially copying and pasting someone else's post, and even then it will highly depend on your chosen technology or what you need to achieve.
What I can say, though, is that, apart from some useful tips you can choose to apply or not depending on what's needed, there are usually some pretty useful functions for handling arrays and avoiding unnecessary uses of foreach blocks, thus making the code a lot easier to read. Some of those functions are map, reduce and filter, which are available in PHP as "array_map", "array_reduce" and "array_filter".
array_map
Given an array, this function will transform its values by using them as a parameter in a given callback function, returning, as a result, a new array with those transformed values just in the same order, and even preserving their keys.
This function will take two parameters as a minimum:
A callback function (or its name, if it's declared somewhere else) that will take as many parameters as many arrays will be used.
An array, whose values will be run through as the first parameter of the callback function.
More arrays, preferably of the same size as the first one, if the callback function accepts more than one parameter.
Let's see an example. The following snippet takes these numbers and sums their squares to their triple value:
function squareAndTripleValue($number) {
return pow($number, 2) + (3 * $number);
}
$numbers = [2, 4, 7, 9, 11, 22];
$tranformedNumbers = array_map("squareAndTripleValue", $numbers);
If we didn't use array_map, the code would come out like this:
function squareAndTripleValue($number) {
return pow($number, 2) + (3 * $number);
}
$numbers = [2, 4, 7, 9, 11, 22];
$tranformedNumbers = [];
foreach ($numbers as $number) {
$tranformedNumbers[] = squareAndTripleValue($number);
}
This one isn't terribly longer than the first one, but imagine you have to do operations like these over and over again. Moreover, running an array through a function by using foreach might be confusing if the array declaration was removed, as it's not really necessary, but it's considered a best practice because it prevents potential issues, especially if the variable is overwritten. Overall, using array_map when possible makes the code look a little bit cleaner and easier to understand.
Here's a repl.it for you to further try it out:
You can check the PHP documentation on array_map here.
array_reduce
This function takes the elements of an array and iterates them through a callback as one of the parameters, where it will be applied to the result of previous elements being applied to an initial value.
It will take two parameters as a minimum:
An array, whose values will be passed as a callback's parameter one at a time.
-
A non-void callback function with two parameters:
- The carry for the initial value or the result from the previous iteration. It should be returned once the second parameter is applied to it.
- The current iterated item inside the array
An initial value, which will be the "carry" value in the first iteration. It's sort of optional since the function will run without it, but you usually shouldn't omit it, as that initial value will be NULL in that case, likely causing problems.
In this example, we'll get the sum of the numbers on an array.
function doSum($carry, $item) {
return $carry + $item;
}
$numbers = [2, 4, 7, 9, 11, 22];
$sum = array_reduce($numbers, "doSum", 0);
And here's the snippet without using array_reduce, but following its style.
function doSum($carry, $item) {
return $carry + $item;
}
$numbers = [2, 4, 7, 9, 11, 22];
$sum = 0;
foreach ($numbers as $number) {
$sum = doSum($sum, $number);
}
Again, the second one isn't terribly longer than the first one, but it's a little bit cleaner and easier to use, provided that you remember what array_reduce does. Regardless, this an example that would work even if the third parameter was omitted and, in fact, would also work in the foreach version if the $sum declaration was omitted. PHP would complain in that case, though. However, as I said earlier, it's not a great idea to do that on a regular basis: just as an example, doing the product of those numbers would require setting the initial value to 1.
You can experiment with array_reduce in this repl.it:
You can check the PHP documentation on array_reduce here.
array_filter
Given an array, this function will return an array with only the values that (or the values whose keys) return TRUE when running through the desired callback if any. Otherwise, a single value won't be part of the resulting array if its key or itself is FALSE when converted to a boolean.
It will take one parameter as a minimum, up to three:
The array to be filtered by the callback function if any.
A callback function that will take one parameter or two.
A special flag that will change this function's behavior. By default, the array's values will be tested against the callback, but if you set the ARRAY_FILTER_USE_KEY constant as the third parameter, it will test the keys, instead. Using the ARRAY_FILTER_USE_BOTH constant will cause the function to send the key as a second parameter along with the value.
Let's do three examples, each one for showing what each constant actually does. In the first one, we'll get just the even numbers (divisible by 2) from the original array:
function testNumber($number) {
return $number % 2 == 0;
}
$numbers = [2, 3, 7, 8, 10, 21, 42, 45];
$evenNumbers = array_filter($numbers, "testNumber");
Here's the same snippet without using array_filter:
function testNumber($number) {
return $number % 2 == 0;
}
$numbers = [2, 3, 7, 8, 10, 21, 42, 45];
foreach ($numbers as $number) {
if (testNumber($number)) {
$evenNumbers[] = $number;
}
}
Just with this example, you can notice that, when using foreach, doing the same as array_filter would do requires the use of an if statement, increasing the number of indentations and making the code clearly harder to read than the array_filter version. Furthermore, keep in mind that the behavior of these two snippets is a bit different, as array_filter always keeps the original key, while foreach would require to declare its usage and assigning it directly inside the block.
In the next snippet, we'll get the numbers with an even key (or index, as PHP assigns incrementing numbers as keys by default to new values). Since the keys start by 0, we're actually obtaining the numbers in an odd position:
function testNumber($number) {
return $number % 2 == 0;
}
$numbers = [2, 3, 7, 8, 10, 21, 42, 45];
$evenNumbers = array_filter($numbers, "testNumber", ARRAY_FILTER_USE_KEY);
And here's the same code without using array_filter:
function testNumber($number) {
return $number % 2 == 0;
}
$numbers = [2, 3, 7, 8, 10, 21, 42, 45];
foreach ($numbers as $key => $number) {
if (testNumber($key)) {
$evenNumbers[$key] = $number;
}
}
And finally, in this snippet, we're going to get only the numbers that are even and have an even key:
function testNumber($number, $key) {
return $number % 2 == 0 && $key % 2 == 0;
}
$numbers = [2, 3, 7, 8, 10, 21, 42, 45];
foreach ($numbers as $key => $number) {
if (testNumber($number, $key)) {
$evenNumbers[$key] = $number;
}
}
Here's a repl.it you can experiment with:
You can check the PHP documentation on array_filter here.
A quick little tip
Before ending, have a look at this snippet:
$template = "Let's suppose you got this %verb% template from a file, a query or whatever";
$verbs = ["nice", "beautiful", "boring", "stupid"];
$fullTexts = array_map(function ($verb) {
return str_replace("%verb%", $verb, $template);
}, $verbs); /*Yes, you can directly define the function as an argument
instead of referencing its name somewhere else*/
If you have been working on PHP for any amount of time, you'll notice the mapping function wouldn't be able to see the $template, thus making the $fullTexts totally empty. That's because all the variables in a function are treated in the function's scope, and the global values outside cannot be seen. We could consider this issue as the biggest downside of using the functions described above instead of foreach.
To solve it, you just have to declare the variable with the global keyword before giving it any use:
$fullTexts = array_map(function ($verb) {
global $template;
return str_replace("%verb%", $verb, $template);
}, $verbs);
You can check more info about the variable scope here.
Conclusion
These three little functions can be quite useful when working on arrays, as they can help make your code a lot easier to read than if using a foreach block for several small tasks like those. However, it would best to overwrite the array you're working on with each step, as chaining those operations would come out like this:
$numbers = [1, 2, 3, 4, 5];
$squaresFromEvenNumbers = array_map(function ($number) {
return pow($number, 2);
}, array_filter($numbers, function ($number) {
return $number % 2 == 0;
}));
That's called callback hell, and it will make the code just as hard to read as if you were just using foreach for these tasks. Even though in JavaScript it's somehow difficult to solve, due to its asynchronous nature, this issue only crops up when functions like these are introduced and are easy to solve: just do each step separately, as I said earlier.
Finally, when should you use each of these functions? Well, as a rule of thumb:
Use array_map when you want an array with the same length as the input array and the resulting elements are the result of a transformation of the input elements.
Use array_filter when you want the same array as the input one, excluding elements that don't fulfill a given condition.
array_reduce should be used when you want to transform an array into a totally different structure or a single value that couldn't be achieved with any other function.
Use foreach only when you need to do something else and the operations on each element don't return a result.
If you want to learn more, don't forget to check out PHP's official documentation on array functions, including array_map, array_reduce, and array_filter.
Posted on January 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.