The PHP array is an extremely versatile creature. You can use it as a map, or a set, or a data object... If you’re brave, you can even use it as an array!

It turns out that treating a PHP array like a traditional array — that is, a list of values indexed by integers starting from 0 — is a dangerous game, full of pitfalls and bugs waiting to happen. This is because PHP arrays aren’t really arrays.

Let’s explore what that means. Here’s a little PHPuzzle to get started: What does this program print out?

Answer: it prints out d !

This code is trying to print the string that comes first in alphabetical order, which would be a . But $sorted[0] doesn’t necessarily mean “first thing in the array”. It means “look up the key 0 and return the value at that key”.

In PHP, arrays are maps (a.k.a. associative arrays). They have keys and values. When you create an array like this:

$ary = ['a', 'b', 'c'];

What’s really happening is this:

$ary = [0 => 'a', 1 => 'b', 2 => 'c'];

PHP is assigning integer keys starting from 0, in sequential order, and so the array acts like a traditional array in many ways.

Try messing with the order of those keys:

If you access this array by its keys, it still acts like a traditional array. But when you iterate through the array, it doesn’t care about the keys. It instead follows the order that the array elements were defined in: b , c , a .

So all PHP arrays have keys, values, and a specific order that those key-value pairs are in. In other words: “An array in PHP is actually an ordered map.”

Going back to the first example: natsort() changes the order that the key-value pairs are in, without changing any of the actual keys:

So if $sorted[0] doesn’t get the first thing in the array, how do we get the first thing? Or say we wanted to get the first 3 things? There are a few options:

array_values()

You can “re-number” an array with array_values(). It returns an array with the same values you passed in, but with the keys re-assigned to integers starting from 0.

array_slice()

Usually when we “index into” an array in PHP, we are really talking about looking up a key. array_slice() is an exception to this — the index you pass it refers to a position in the array, rather than a key.

A caveat here is that it will still preserve string keys, unlike array_values().

reset(), next(), and friends

Every array in PHP has an internal pointer which can be used to iterate over the array. reset() sets the internal pointer to the beginning of the array. next() advances it to the next key-value pair. There’s also prev(), key(), current(), and end(). This low-level API is ugly, but it does give you a good feel for how PHP arrays really work:

You almost never have to use these functions, but you’ll still sometimes see reset() used as a convenient way to grab the first value of an array, or end() to grab the last value.

Laravel’s collect()

Laravel has an excellent collections class which has been extracted to a standalone library, which is what we use at 7shifts.

It provides first() and last() methods to get the first and last elements of the array, and a take() method to get the first N elements.

Notice that take() preserves the keys of the array. Many PHP array functions accept an optional argument called $preserve_keys . This collections library almost always passes true for this argument when wrapping PHP array functions, so you can always expect the keys to be preserved when using this library.

A few tips for working with PHP arrays

Here are a few practical lessons I’ve learned about working with PHP arrays.

Always be suspicious of $ary[0]

Any time you see an array index of 0 , that code is likely assuming that the array is behaving like a traditional array. Often this is an unsafe assumption to make, especially if the array was passed into your function from elsewhere. 0 might not be the first key in the array, or it might not even exist (after an array_filter() for example).

Unless you just created the array yourself, you should probably use reset($ary) or a library method like collect($ary)->first() instead.

Consider re-numbering your arrays

You can attack the $ary[0] problem from the other side as well: Whenever you pass an array to another function, make sure it’s keyed like a traditional array. This usually means calling array_values() right after doing an array_filter() , or after any operation that might remove or reorder the keys on you.

You could argue that constantly “fixing” arrays with array_values() is just fighting against their true nature. Instead of forcing them to act like traditional arrays, perhaps we should accept PHP arrays for what they are and all just agree to write code that never assumes what the keys of an array are. But this will always be a point of confusion for newcomers to the language who’ll assume — understandably — that PHP arrays are like traditional arrays.

The above two points follow the robustness principle: “Be conservative in what you send, be liberal in what you accept from others.”

Keys matter when comparing arrays, but order doesn’t

Tests like assertEquals(['a', 'b'], array_filter(['a', null, 'b'])) will fail because [0 => 'a', 1 => 'b'] does not equal [0 => 'a', 2 => 'b'] ! The keys have to match.

On the other hand, assertEquals($ary, collect($ary)->sort()->all()) will always pass because the order of the key-value pairs doesn’t make a difference to == . For example, [0 => 'a', 1 => 'b'] == [1 => 'b', 0 => 'a'] is true.

In both cases, you probably want to use array_values() to re-number the array before comparing them.

Using arrays as maps

Say you have a $users_by_id array, where $users_by_id[32] would look up the User with an id of 32. Then if you map that array using array_map(function ($user) { return $user->getName(); }) , you’ll get an array of names that’s also keyed by user id, because array_map() doesn’t change the keys of the array. It can be very useful to preserve the keys like this, so when you’re working with maps you may want to avoid using array functions that re-number the array.

Using arrays as sortable maps

You can sort the $users_by_id array and then use it to both (1) look up users by their id and (2) iterate through users in a specific order. This trick saves you from having to maintain a separate array of ids just to keep track of their order, which is what you’d have to do in some other languages.

Communicate what type of array your array is

Much of the confusion with PHP arrays comes down to the fact that it’s one data structure trying to serve two purposes. PHP’s type system may not differentiate between traditional arrays and maps, but we should. In the absence of a proper type system, we can use variable-naming conventions to communicate types. For example, names of traditional arrays are often plural nouns like $users , and names of maps often end with “by x”, like $users_by_id .

Conclusion

Mental models are important. I used to think of a PHP array as just a bag of key-value pairs. When I realized that they are actually a list of key-value pairs in a specific order, that seemed to open the door to really getting PHP arrays. There is much more to learn about arrays and the quirks of all the array functions in the PHP docs, and I’ve found that going in with the right mental model helps these quirks to make a lot more sense!