People often ask me, "how do I specify keys when I'm mapping a collection?"
It actually ends up being a pretty interesting topic, so I decided to cover it in a short screencast, as well as in written format below.
Say you had a list of employees and you needed to transform it into a lookup table mapping their email addresses to their names.
Given this input...
$employees = [
[
'name' => 'John',
'department' => 'Sales',
'email' => 'john@example.com'
],
[
'name' => 'Jane',
'department' => 'Marketing',
'email' => 'jane@example.com'
],
[
'name' => 'Dave',
'department' => 'Marketing',
'email' => 'dave@example.com'
],
];
...we need to create an associative array that looks like this:
$emailLookup = [
'john@example.com' => 'John',
'jane@example.com' => 'Jane',
'dave@example.com' => 'Dave',
];
How can we do this using collections and higher order functions?
Map to Nowhere
Since we know we need to transform each element in the first array into a new element in the associative array, the first thing we might try is to use the map
operation:
$emailLookup = $employees->map(function ($employee) {
return $employee['name'];
});
This does give us a list of employee names, but we have no way of setting the keys!
Back to the drawing board...
PHP's Array Identity Crisis
This problem of wanting to customize keys during a map operation is something I get asked about pretty regularly.
I think the reason it seems like a tricky problem is because in PHP, we use the same data type to represent both a list and a dictionary.
Forget about PHP for a minute and pretend we were trying to solve this problem in JavaScript.
We'd start with a similar array of employee objects:
const employees = [
{
'name': 'John',
'department': 'Sales',
'email': 'john@example.com'
},
{
'name': 'Jane',
'department': 'Marketing',
'email': 'jane@example.com'
},
{
'name': 'Dave',
'department': 'Marketing',
'email': 'dave@example.com'
},
];
...but our desired output would be an object like this:
const emailLookup = {
'john@example.com': 'John',
'jane@example.com': 'Jane',
'dave@example.com': 'Dave',
};
So we wouldn't actually be transforming one array into another array, we would be reducing our initial array into a single object.
Implementing this transformation in JavaScript with reduce
would look like this:
const emailLookup = employees.reduce(function (emailLookup, employee) {
emailLookup[employee.email] = employee.name;
return emailLookup;
}, {});
Here's the same thing in PHP using Laravel's Collection library:
$emailLookup = $employees->reduce(function ($emailLookup, $employee) {
$emailLookup[$employee['email']] = $employee['name'];
return $emailLookup;
}, []);
A Reusable Abstraction
Even though we've solved our original problem, I'm not totally satisfied with how the code reads.
Something I mention in Refactoring to Collections is that I often see reduce
as a sign that I'm missing a more expressive abstraction built on top of reduce
.
It would be great if we could create an operation like toAssoc()
that could use to transform a list into an associative array, but how could we specify both the key and the value?
Learning from Other Languages
The Ruby equivalent of an associative array is a Hash.
You can transform an Enumerable into a Hash by calling the to_h
method, as long as the enumerable is made of [key, value]
pairs.
That means given an array that looks like this:
employees = [
['john@example.com', 'John'],
['jane@example.com', 'Jane'],
['dave@example.com', 'Dave'],
]
...we can turn that into an email lookup hash by calling to_h
:
employees.to_h
# => {
# 'john@example.com' => 'John',
# 'jane@example.com' => 'Jane',
# 'dave@example.com' => 'Dave',
# }
Let's implement this with Laravel's Collection class macros!
The toAssoc Macro
Since Collections are macroable, we can add our implementation of toAssoc
directly to the Collection class:
Collection::macro('toAssoc', function () {
return $this->reduce(function ($assoc, $keyValuePair) {
list($key, $value) = $keyValuePair;
$assoc[$key] = $value;
return $assoc;
}, new static);
});
Now as long our data is structured as [key, value]
pairs, we can use toAssoc
to transform it into an associative array:
$emailLookup = collect([
['john@example.com', 'John'],
['jane@example.com', 'Jane'],
['dave@example.com', 'Dave'],
])->toAssoc();
// => [
// 'john@example.com' => 'John',
// 'jane@example.com' => 'Jane',
// 'dave@example.com' => 'Dave',
// ]
So how do we get our original list of employees into a list of [key, value]
pairs?
Mapping to Pairs
Map
of course! Here's what the whole thing would look like:
$emailLookup = collect([
[
'name' => 'John',
'department' => 'Sales',
'email' => 'john@example.com'
],
[
'name' => 'Jane',
'department' => 'Marketing',
'email' => 'jane@example.com'
],
[
'name' => 'Dave',
'department' => 'Marketing',
'email' => 'dave@example.com'
],
])->map(function ($employee) {
return [$employee['email'], $employee['name']];
})->toAssoc();
If we wanted to take it a step further, we could even define a mapToAssoc
operation that lets us do the transformation in one step:
Collection::macro('mapToAssoc', function ($callback) {
return $this->map($callback)->toAssoc();
});
...which we could use like this:
$emailLookup = $employees->mapToAssoc(function ($employee) {
return [$employee['email'], $employee['name']];
});
Pretty slick!
Learning More
If you enjoyed this post, you might be interested in the book + video course I recently released.
"Refactoring to Collections" is an in-depth guide to understanding and implementing functional programming ideas in PHP.
It covers fundamental topics like imperative vs. declarative programming, understanding higher order functions, and includes 4 hours of screencasts going over refactorings like the one in this post.
Check out the website to learn more, or subscribe below for a free chapter sample.