PHP Arrays — Off-by-One Errors That Null Prices
A count($items) off-by-one error nulled every 10th price.
20+ years shipping production PHP systems at scale. Written from production experience, not tutorials.
- PHP arrays store multiple values under a single variable using integer or string keys
- Indexed arrays: auto-numbered from 0, best for ordered lists
- Associative arrays: custom string keys, ideal for structured records
- Multidimensional arrays: arrays inside arrays, like a spreadsheet
- Copy-on-write means assigning an array to another variable doesn't duplicate it until you modify one
- Performance: count() is O(1) but array_shift on large arrays is O(n) — avoid in loops
- Biggest mistake: off-by-one from zero-based indexing or using loose comparison (==) when strict (===) is needed
Imagine you're packing for a road trip. Instead of carrying one item in each hand, you grab a suitcase and pack everything inside — shirts in one slot, shoes in another, snacks in a zip pocket. A PHP array is that suitcase: one variable that holds multiple values, each stored in its own labelled slot. Without arrays, you'd need a separate variable for every piece of data — like carrying each item of clothing loose in your arms. Nobody wants that.
Every real application deals with lists. A shopping cart holds multiple products. A blog has multiple posts. A user profile stores multiple preferences. If PHP only let you store one value per variable, you'd be writing hundreds of lines just to hold a handful of items — and your code would collapse the moment the list grew or shrank. Arrays are how PHP solves this problem, and they're used in virtually every PHP script ever written.
The specific problem arrays fix is called 'data grouping'. Instead of writing $product1, $product2, $product3 and scrambling to keep track of them all, you write one $products array and let PHP manage the collection for you. You can add to it, remove from it, loop over it, sort it, search it — all with built-in tools PHP provides out of the box.
By the end of this article you'll know how to create all three types of PHP arrays (indexed, associative, and multidimensional), how to read and update values inside them, how to loop through them, and — crucially — the exact mistakes that trip up beginners so you can dodge them from day one.
What PHP Arrays Actually Are — Ordered Hash Maps in Disguise
PHP arrays are ordered hash maps that combine the features of a list, dictionary, and sparse vector into a single data structure. Internally, each array is a HashTable of buckets, where each bucket stores a key-value pair and a link to the next bucket in insertion order. This dual nature — O(1) key lookup via hashing plus O(1) iteration in insertion order — is both their superpower and the root of subtle bugs.
Every array maintains an internal pointer and a linear order of elements, independent of key values. When you append with $arr[] = $val, PHP assigns the next integer key as max(int_keys) + 1, not count($arr). This means after unset() on a non-last element, the next append reuses the highest existing integer key plus one, leaving gaps. Array functions like array_values() re-index to close gaps, but unset() does not — a common source of off-by-one errors in loops that assume contiguous 0-based keys.
Use PHP arrays when you need a flexible, mixed-type collection that preserves insertion order and supports both integer and string keys. They are the default choice for 90% of data structures in PHP applications — from HTTP query parameters to database result sets. However, for large datasets (10k+ elements) or strict typed collections, consider SplFixedArray or array_map/array_filter to avoid the overhead of hash table lookups and the mental cost of tracking key gaps.
isset() or array_key_exists() before arithmetic — never assume a key exists because the schema says it should.isset() before using a value in arithmetic.Unset() leaves gaps; use array_values() to re-index if you need contiguous integer keys.Indexed Arrays — A Numbered List PHP Can Remember
An indexed array stores values in numbered slots, starting at position 0. Think of it like a numbered queue at a bakery: the first person is ticket 0, the second is ticket 1, and so on. The number is called the index or key, and the thing stored there is the value.
Why does counting start at 0? It's a convention inherited from low-level computing, and PHP follows it like almost every other language. It trips up beginners exactly once — then you never forget it.
You create an indexed array using the square-bracket syntax (preferred in modern PHP) or the older array() function. Both work, but square brackets are cleaner and what you'll see in professional codebases today.
To get a value out, you write the variable name followed by the index in square brackets: $fruits[0] gives you the first item. To add a new item to the end, use $fruits[] = 'mango' — the empty brackets tell PHP to append automatically. You don't need to know what the next index number is; PHP figures it out.
Beyond basic operations, you'll often need to extract portions of an array. returns a subset without modifying the original. array_slice() removes a portion and optionally replaces it — it modifies the original. array_splice() splits an array into groups of a given size, handy for pagination.array_chunk()
end() function.array_key_first() if you're not sure.end() for the last element.Associative Arrays — Give Your Data a Meaningful Label
An indexed array is great for ordered lists, but what if you want to store a person's profile? You'd need to remember that index 0 is the name, index 1 is the email, index 2 is the age — and that's a disaster waiting to happen.
Associative arrays solve this by letting you choose your own keys instead of relying on numbers. Think of it like a form with labelled fields: 'Name: Sarah', 'Email: sarah@example.com', 'Age: 28'. Each label (key) maps directly to its value. This is how PHP stores structured data — user records, configuration settings, API responses.
You create an associative array using the same square-bracket syntax, but you define the key explicitly using the => arrow operator. On the left of => is the key (a string), on the right is the value.
Associative arrays are the backbone of most real PHP applications. When PHP reads a submitted HTML form, it hands you the data as an associative array in $_POST. When you decode a JSON API response, you get an associative array. When you fetch a database row with PDO, it comes back as an associative array. You'll use these constantly.
Two legacy functions worth knowing: creates an associative array from variable names, and compact() does the reverse (but is notorious for security issues and should be avoided). For modern code, just build arrays manually.extract()
array_key_exists() when NULL is a valid value, isset() otherwise.Multidimensional Arrays — Arrays Inside Arrays
So far each array has stored simple values — strings and numbers. But what if you want to store a list of users, and each user has their own profile data? You put arrays inside arrays. That's a multidimensional array, and it's less scary than it sounds.
Think of a spreadsheet. Each row is a user, each column is a piece of data (name, email, age). A multidimensional array works exactly the same way: the outer array is the list of rows, and each inner array is one row of data.
To access a value, you chain square brackets: $users[0]['name'] means 'go to the first user (index 0), then get the name key from their profile'. It reads left-to-right, outer-to-inner.
Multidimensional arrays are everywhere in PHP. Database query results return as an array of associative arrays. JSON from an API decodes into nested arrays. Shopping cart data is a list of product arrays. Once you're comfortable with one level of nesting, the rest follows the exact same logic — just add another bracket.
For deep nesting, PHP provides array_walk_recursive() which visits every leaf, and array_map_recursive() (custom function). But deep nested structures often benefit from being modeled as objects for clarity.
The Most Useful PHP Array Functions You'll Actually Use
PHP ships with over 70 built-in array functions. You don't need to memorise all of them — but a handful come up in almost every project. Knowing these saves you from writing loops by hand for tasks PHP already solved.
Here's the honest shortlist: count() tells you how many items are in an array. in_array() checks whether a value exists anywhere in an array. array_push() adds items to the end (though the [] shorthand is more common). array_pop() removes and returns the last item. array_merge() combines two arrays into one. array_filter() removes items that don't pass a test. array_map() transforms every item using a function. sort() sorts an indexed array alphabetically or numerically. ksort() sorts an associative array by its keys.
The most important mental model: some functions return a new array (array_map, array_filter, array_merge) while others modify the original array in place (sort, ksort, array_push). This distinction matters — if you expect a sorted copy but get NULL because you forgot to use the return value, you'll be confused for longer than you should be.
Additional functions: and array_sum() compute totals directly. array_product() is the Swiss Army knife for reducing an array to any single value (sum, join, maximum). array_reduce() extracts a column from a multidimensional array, very handy from database results.array_column()
$result = sort($arr) in a pull request, flag it immediately.PHP Array Functions Quick Reference Table
Here is a quick reference table of the most commonly used PHP array functions. Keep this handy while coding.
| Function | Description | Return Value | Modifies Original? |
|---|---|---|---|
| --- | --- | --- | --- |
count($arr) | Returns the number of elements in an array | int | No |
in_array($needle, $haystack, $strict) | Checks if a value exists in an array | bool | No |
array_push($arr, ...$values) | Adds one or more elements to the end of an array | int (new length) | Yes |
array_pop($arr) | Removes and returns the last element | mixed | Yes |
array_merge(...$arrays) | Merges one or more arrays into one | array | No |
array_search($needle, $haystack, $strict) | Searches array for a given value and returns the first key | mixed (key or false) | No |
array_map($callback, $arr) | Applies a callback to every element; returns a new array | array | No |
array_filter($arr, $callback) | Filters elements via a callback; returns a new array | array | No |
array_reduce($arr, $callback, $initial) | Iteratively reduces the array to a single value via a callback | mixed | No |
sort($arr) | Sorts an indexed array in ascending order | bool (true on success) | Yes |
rsort($arr) | Sorts an indexed array in descending order | bool | Yes |
ksort($arr) | Sorts an associative array by key in ascending order | bool | Yes |
krsort($arr) | Sorts an associative array by key in descending order | bool | Yes |
asort($arr) | Sorts an associative array by value in ascending order, preserving keys | bool | Yes |
arsort($arr) | Sorts an associative array by value in descending order, preserving keys | bool | Yes |
array_unique($arr, $flags) | Removes duplicate values from an array | array | No (returns new) |
array_key_exists($key, $arr) | Checks if the specified key exists in the array | bool | No |
array_keys($arr) | Returns all the keys of an array | array | No |
array_values($arr) | Returns all the values of an array | array | No |
implode($glue, $pieces) | Joins array elements with a string (aliased as join) | string | No |
Keep in mind that the $strict parameter for in_array and array_search defaults to false (loose comparison). Always pass true unless you intentionally need type coercion.
Also: array_sum and array_product are O(n) but very fast. array_column is a gem for extracting columns from multidimensional arrays.
array_push and array_pop modify in place — but the pattern holds for array_map, array_filter, array_merge, array_reduce, array_unique, array_keys, array_values. If the function name begins with array_ and ends with something other than push/pop/shift/unset, it generally returns a new array without touching the original.sort, rsort, ksort, asort, arsort, array_push, or array_pop is assigned to a variable — that's almost always wrong.Array Destructuring — Extract Array Values into Variables
PHP's array destructuring (or unpacking) lets you assign array elements directly to variables in one statement. This is a clean alternative to manual indexing and is especially useful when working with functions that return arrays, like database fetches or pathinfo().
PHP offers two syntaxes: the classic function (available since PHP 4) and the shorter square bracket syntax (since PHP 7.1). Both do the same thing, but the list()[] syntax is now preferred for its brevity and clarity.
Destructuring works with both indexed and associative arrays. For indexed arrays, PHP assigns variables in order starting from index 0. For associative arrays, you specify the key as the variable name, making it extremely readable.
One common gotcha: only assigns from numeric indices by default. To destructure associative arrays, you must use the list()[] syntax with named keys (PHP 7.1+). Also, you can skip elements by leaving a blank — [, $b, , $d] = $arr — which is great when you only need specific items.
Nested destructuring works too: [$a, [$b, $c]] = $arr.
[] syntax is shorter, more intuitive, and supports associative destructuring. If you need to support PHP 7.0 or earlier, stick with list(). But in any modern codebase (PHP 7.1+), the square bracket approach is the standard.list() extracts array values directly into variables. Use [] for PHP 7.1+ and take advantage of associative key matching to improve readability.Sorting Arrays in PHP (sort, rsort, ksort, usort)
PHP provides many sorting functions to arrange array elements in a specific order. The key is choosing the right function for your array type and desired outcome.
For indexed arrays, sorts in ascending order, and sort() sorts in descending order. Both discard existing keys and re-index numerically. This is fine when you don't care about preserving the original keys.rsort()
For associative arrays, sorts by key (ascending) and ksort() by key (descending), preserving key-value associations. krsort() sorts by value (ascending) and asort() by value (descending), also preserving keys. These are essential when you need to maintain logical relationships (e.g., sorting a user array by name while keeping each person's full data intact).arsort()
For custom sorting logic, accepts a user-defined comparison function. You can sort by any criteria — by the length of a string, by a nested property, or by a computed value. The comparison function must return an integer less than, equal to, or greater than zero.usort()
All sorting functions modify the array in place and return a boolean. They do not return a sorted copy. The only way to keep the original order is to clone the array first: $sorted = $arr; sort($sorted);.
For stable sorting, use and uasort() for associative arrays with custom comparison.uksort()
$copy = $arr; sort($copy);. This is the most common sorting mistake in PHP.usort with a complex callback can be slow. For stable sorting, consider array_multisort.sort()/rsort() for indexed arrays, ksort()/asort() for associative arrays, and usort() for custom logic. All modify in place — copy first if you need the unsorted version.Spread Operator in Arrays (PHP 7.4+)
Introduced in PHP 7.4, the spread operator (...) allows you to unpack an array into another array. This is similar to but with a cleaner syntax and better performance in some cases.array_merge()
You use it inside a new array literal by prefixing the array variable with .... All values from the spread array are copied at that position. You can use the spread operator multiple times and combine it with regular elements.
One important behaviour: the spread operator only works with arrays and objects that implement Traversable (like ArrayIterator). It does not work with strings or non-traversable objects.
Another difference from : with string keys, the spread operator behaves identically — later keys overwrite earlier ones. With integer keys, array_merge() re-indexes them starting from 0, while the spread operator preserves integer keys from the source array (and if there's a conflict, the later key overwrites the earlier one, just like string keys). This subtle difference matters when merging indexed arrays where you want to keep original indices.array_merge()
array_merge() for arrays with fewer than ~10,000 elements. For very large arrays, the difference is negligible. The spread operator also makes the intent clearer — you can see exactly where elements are being inserted.Practice Exercises: Test Your PHP Array Skills
Apply what you've learned with these five real-world exercises. Each exercise comes with a description, hints, and the expected output. Try to solve each one without peeking at the solution code first.
Exercise 1: Shopping Cart Manipulation You have an array of product names in a cart. Write a function that adds a product to the cart, removes a product by name, and returns the updated cart. Assume products can appear multiple times (different quantities), but for this exercise each product appears once. Use array_push or [] to add, array_search to find the position, and unset plus array_values to re-index.
Exercise 2: Grade Sorter Given an associative array of student names and their integer grades (e.g., ['Alice' => 85, 'Bob' => 92, 'Carol' => 78]), sort the array by grade in descending order while preserving the student names as keys. Use . Then display each student's name and grade.arsort()
Exercise 3: Array Deduplication Write a function that removes duplicate values from an array while keeping the first occurrence of each value. Use . Test with a mix of integers and strings (with loose comparison issues). Then write a version that uses strict comparison. Compare results.array_unique()
Exercise 4: Nested Array Flattening Write a recursive function that flattens a multidimensional array into a single-level indexed array. For example, [1, [2, [3, 4]], 5] becomes [1, 2, 3, 4, 5]. Do not use array_merge_recursive — implement it manually using a foreach loop and recursion.
Exercise 5: Array Partitioning Given an array of numbers, partition it into two arrays: one with even numbers and one with odd numbers. Use with a callback or manually loop. Then output both arrays.array_filter()
Bonus Exercise: Rotate Array Write a function that rotates an array by a given number of positions. For example, rotate([1,2,3,4,5], 2) gives [4,5,1,2,3]. Use array_slice and array_merge.
```php <?php // --- Exercise 1: Shopping Cart --- $cart = ['apple', 'banana', 'cherry']; function addProduct(array $cart, string $product): array { $cart[] = $product; return $cart; } function removeProduct(array $cart, string $product): array { $index = array_search($product, $cart, true); if ($index !== false) { unset($cart[$index]); $cart = array_values($cart); // re-index } return $cart; } $cart = addProduct($cart, 'date'); $cart = removeProduct($cart, 'banana'); print_r($cart); // ['apple', 'cherry', 'date']
// --- Exercise 2: Grade Sorter --- $grades = ['Alice' => 85, 'Bob' => 92, 'Carol' => 78]; arsort($grades); foreach ($grades as $name => $grade) { echo "$name: $grade "; } // Bob: 92 // Alice: 85 // Carol: 78
// --- Exercise 3: Deduplication --- $data = [1, 2, 2, 3, '2', 4, 4]; $uniqueLoose = array_unique($data); // loose comparison $strictUnique = []; foreach ($data as $value) { if (!in_array($value, $strictUnique, true)) { $strictUnique[] = $value; } } print_r($uniqueLoose); print_r($strictUnique);
// --- Exercise 4: Flatten --- function flatten(array $arr): array { $result = []; foreach ($arr as $item) { if (is_array($item)) { $result = array_merge($result, flatten($item)); } else { $result[] = $item; } } return $result; } $nested = [1, [2, [3, 4]], 5]; print_r(flatten($nested)); // [1,2,3,4,5]
// --- Exercise 5: Partition --- $numbers = [1, 2, 3, 4, 5, 6]; $even = []; $odd = []; foreach ($numbers as $num) { if ($num % 2 == 0) { $even[] = $num; } else { $odd[] = $num; } } echo "Even: " . implode(', ', $even) . " "; echo "Odd: " . implode(', ', $odd) . " ";
// Bonus: Rotate function rotate(array $arr, int $positions): array { $positions = $positions % count($arr); $slice = array_slice($arr, -$positions); $remainder = array_slice($arr, 0, count($arr) - $positions); return array_merge($slice, $remainder); } print_r(rotate([1,2,3,4,5], 2)); // [4,5,1,2,3] ```
Try these exercises before reviewing the solution code. They will reinforce the array functions, sorting, destructuring, and traversal patterns covered in this article.
array_walk_recursive or a simple foreach. But understanding the underlying logic helps you debug when something unexpected happens.Array Traversal and Mutation Pitfalls
Looping over an array while modifying it leads to surprising bugs. The classic example is using foreach ($arr as &$value) — the reference persists after the loop ends, and any subsequent assignment to that variable modifies the array.
Another trap: calling on an element inside a unset()foreach loop can skip the next element or cause undefined behaviour depending on how the loop tracks the internal array pointer. PHP's foreach operates on a copy of the array by default, so unsetting inside the loop doesn't break the iteration — but it can confuse readers.
Using or array_shift() inside a loop is also dangerous because they reset the internal pointer. If you need to remove items during iteration, collect the keys first with array_pop(), then remove them after the loop.array_keys()
Also, after unsetting elements, the array remains sparse. Use to re-index if you need contiguous numeric keys.array_values()
Type Juggling and Loose Comparison in Arrays
PHP is dynamically typed and famously loose with comparisons. When you use , in_array(), or array_search() without strict mode, type coercion can give you false positives or unexpected behaviour.array_unique()
Example: in_array('1', [1, 2, 3]) returns true because the string '1' is coerced to integer 1 during the loose comparison. This can cause security issues if you're checking user input against a whitelist.
Similarly, uses loose comparison by default, so array_unique()['1', 1, 2] becomes ['1', 2] — the integer 1 is considered equal to the string '1', and only the first occurrence is kept.
The fix is always to pass the third parameter true for strict comparison: in_array($needle, $haystack, true). For , use array_unique()SORT_REGULAR with the flag SORT_STRING or convert to a consistent type first.
To avoid type juggling altogether, declare strict_types=1 at the top of your PHP files. This forces PHP to throw a TypeError when you pass a string to a function expecting an int, among other things.
true as third param to in_array, array_search, array_keys.Array Walk and Reduce — Advanced Iteration
Beyond foreach, PHP offers two powerful iteration functions: and array_walk().array_reduce()
array_walk($arr, $callback) applies a callback to each element, modifying the array in place if the callback uses a reference. Unlike array_map, which returns a new array, array_walk doesn't return an array — it returns true on success. Use it when you need to mutate the original or when the callback doesn't produce a return value.
array_reduce($arr, $callback, $initial) iteratively reduces the array to a single value. The callback receives a carry (accumulator) and the current item. The initial value is used as the first carry. It's like a left fold in functional programming.
Both functions are part of PHP's functional programming toolkit, but they're more verbose than foreach for simple cases. Use them when you want to express intent clearly or when chaining transformations.
array_walk with a reference is explicit. But most developers find foreach clearer. For simple transformations, array_map is often preferred because it returns a new array and doesn't mutate. array_reduce shines when you need a single aggregated result.array_reduce is commonly used in financial calculations where you need to accumulate values across a dataset — summing invoice totals, computing running balances, etc.PHP Array Performance: Big O and Memory
Not all array operations are created equal. Understanding the time complexity of common operations helps you avoid slow code.
is O(1) — PHP caches the array length.count()- Accessing by key/index is O(1) — PHP arrays are hash tables.
is O(n) — it scans the entire array. For large lookups, usein_array()array_flip()to create a hash map first.is O(n) — same as in_array.array_search()is O(n log n) — Quicksort implementation.sort()is O(1) amortized.array_push()is O(n) — it re-indexes all numeric keys.array_shift()is O(n) — same reason.array_unshift()is O(n log n) due to sorting.array_unique()
Memory: PHP arrays are memory-heavy. Each entry consumes ~144 bytes for an integer value, more for strings. For large datasets (millions of elements), consider SplFixedArray which uses a C array internally and is much more memory efficient.
Creating Arrays the Right Way — array() vs []
When you need a container, you have two options: the old array() syntax or the short []. There's no performance difference. PHP parses both into the same internal hash map. The choice is readability and muscle memory.
Short syntax wins in modern PHP. It's cleaner for nested structures. It matches JSON. It signals you're writing current code, not legacy PHP 4.
But here's the trap: trailing commas. PHP 7.3+ lets you add a trailing comma in function calls. PHP 8.0+ extends that to arrays. Use it. When you add an element later, you change one line instead of two. Your diff stays clean.
When initializing, prefer empty brackets for an empty array: $items = []. Then push with $items[] = $value. Avoid array() — it's visual noise.
array() is a language construct — you can't pass it as a callback. [] is syntactic sugar. For closures like array_map(fn($x) => [$x], $items), [] works; array() doesn't.Adding and Removing Elements Without Losing Your Mind
Adding elements seems trivial until you accidentally overwrite data or introduce gaps. Here's the brutal truth: PHP gives you tools that look similar but behave differently.
To append, use $array[] = $value. It picks the next numeric key automatically. But only for numeric keys. If your array has string keys, [] appends at the next numeric index, leaving your associative data untouched. That's usually a bug.
For associative arrays, always use explicit assignment: $array['key'] = $value. Don't trust autovivification.
Removing elements is where juniors get burned. unset($array[3]) removes the element but leaves the key. 99% of the time you want array_splice() — it re-indexes numeric keys. For associative arrays, unset() is fine because key order doesn't matter.
array_pop() removes the last element. array_shift() removes the first and re-indexes. Both modify the original array. They're not pure functions.
unset() does not. If you unset index 3 of a 5-element array, you still have keys 0,1,2,4. Iterating with foreach will hit them all, but array_values() will reset.array_splice() to remove. For associative, use unset(). Never mix numeric and string keys unless you enjoy debugging.Off-by-One Index Error Took Down the Pricing API at 2 PM
$items[count($items)] instead of $items[count($items) - 1] in a loop that assigned prices. When the loop hit the last index, it accessed out of bounds, returning NULL for that slot.$items[count($items)] with end($items) or use $items[count($items) - 1]. Add an explicit check for array emptiness before accessing any index.- Zero-based indexing is the single most common array bug in PHP.
- Never assume the last index equals the count — always subtract one.
- Use
orend()for the last element.array_key_last()
array_key_exists() or isset(). Remember: isset() returns false if the value is NULL, while array_key_exists() returns true even for NULL values.foreach ($arr as &$value), the reference persists after the loop. Add unset($value) immediately after the loop to break the reference.true to force strict type comparison: in_array($needle, $haystack, true).sort($arr); standalone, then use $arr. To keep the original, copy first: $sorted = $arr; sort($sorted);.print_r($arr);echo 'Last index: ' . (count($arr) - 1);Common mistakes to avoid
4 patternsOff-by-one index errors
Forgetting that sort() modifies the array in place
Using == instead of === when searching arrays
Foreach with &$value without unsetting after the loop
Interview Questions on This Topic
What is the difference between array_key_exists() and isset() when checking array keys?
array_key_exists($key, $arr) returns true if the key exists in the array, regardless of its value. isset($arr[$key]) returns false if the key exists but its value is NULL. So if you're expecting NULL as a valid value, use array_key_exists. Also, isset is faster but has the NULL quirk. In PHP 8, isset does not throw a warning for undefined keys, while array_key_exists does not either—but conceptually they differ.20+ years shipping production PHP systems at scale. Written from production experience, not tutorials.
That's PHP Basics. Mark it forged?
18 min read · try the examples if you haven't