Beginner 13 min · March 06, 2026

PHP Arrays — Off-by-One Errors That Null Prices

A count($items) off-by-one error caused null prices on every 10th product.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
  • Biggest mistake: off-by-one from zero-based indexing or using loose comparison (==) when strict (===) is needed
Plain-English First

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.

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.

indexed_array_basics.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

// Create an indexed array of fruit names
// PHP automatically assigns index 0, 1, 2 to each item
$fruits = ['apple', 'banana', 'cherry'];

// Access the first item — remember, counting starts at 0!
echo $fruits[0]; // apple
echo "\n";

// Access the third item using index 2
echo $fruits[2]; // cherry
echo "\n";

// Add a new item to the END of the array
// Empty [] tells PHP: 'put this at the next available index'
$fruits[] = 'mango';

// Count how many items are in the array
$totalFruits = count($fruits);
echo "Total fruits: " . $totalFruits; // 4
echo "\n";

// Loop through every item using a foreach loop
// $fruit (singular) is a temporary variable that holds each item one-by-one
foreach ($fruits as $fruit) {
    echo "- " . $fruit . "\n";
}

// Update an existing item — overwrite index 1
$fruits[1] = 'blueberry';
echo "Updated index 1: " . $fruits[1]; // blueberry
echo "\n";

// Print the full array structure for debugging
print_r($fruits);
Output
apple
cherry
Total fruits: 4
- apple
- banana
- cherry
- mango
Updated index 1: blueberry
Array
(
[0] => apple
[1] => blueberry
[2] => cherry
[3] => mango
)
Watch Out: Indexes Start at 0, Not 1
If your array has 4 items, the last one is at index 3 — not 4. Trying to access $fruits[4] on a 4-item array gives you NULL and a Notice in PHP 8. Always use count($array) - 1 to safely grab the last item, or better yet, use the end() function.
Production Insight
In production, you'll often iterate over arrays from an external API where the index starting point is unknown.
Always assume zero-based and validate with array_key_first() if you're not sure.
Rule: when a price comes back NULL, don't blame the database — check your index first.
Key Takeaway
Indexed arrays start at 0.
Count gives you the total number, not the last index.
Add items with $arr[] and use 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.

associative_array_basics.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

// Create an associative array to store a user's profile
// The string on the LEFT of => is the key
// The value on the RIGHT of => is what's stored
$userProfile = [
    'name'    => 'Sarah',
    'email'   => 'sarah@example.com',
    'age'     => 28,
    'country' => 'Canada'
];

// Access a value using its KEY — no index numbers needed
echo $userProfile['name'];    // Sarah
echo "\n";
echo $userProfile['email'];   // sarah@example.com
echo "\n";

// Update a value — same syntax, just assign a new value
$userProfile['age'] = 29;
echo "Updated age: " . $userProfile['age']; // 29
echo "\n";

// Add a brand new key-value pair at any time
$userProfile['role'] = 'editor';

// Loop through an associative array using foreach
// $key holds the label, $value holds the data
foreach ($userProfile as $key => $value) {
    echo $key . ': ' . $value . "\n";
}

// Check if a specific key EXISTS before using it
// This prevents errors when data might be missing
if (array_key_exists('email', $userProfile)) {\n    echo \"Email is set: \" . $userProfile['email'] . \"\\n\";\n}\n\n// Get all keys as a separate array\n$profileKeys = array_keys($userProfile);\nprint_r($profileKeys);\n",
        "output": "Sarah\nsarah@example.com\nUpdated age: 29\nname: Sarah\nemail: sarah@example.com\nage: 29\ncountry: Canada\nrole: editor\nEmail is set: sarah@example.com\nArray\n(\n    [0] => name\n    [1] => email\n    [2] => age\n    [3] => country\n    [4] => role\n)"
      }

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.

multidimensional_array_basics.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

// A list of users — each user is its own associative array
// The outer array uses automatic numeric indexes (0, 1, 2)
$users = [\n    [\n        'name'  => 'Alice',\n        'email' => 'alice@example.com',\n        'role'  => 'admin'\n    ],
    [\n        'name'  => 'Bob',\n        'email' => 'bob@example.com',\n        'role'  => 'editor'\n    ],
    [\n        'name'  => 'Carol',\n        'email' => 'carol@example.com',\n        'role'  => 'viewer'\n    ]
];

// Access a single value — outer index first, then inner key
// Read as: "user at position 0, then their name"
echo $users[0]['name'];  // Alice
echo "\n";

// Access Bob's email: position 1, key 'email'
echo $users[1]['email']; // bob@example.com
echo "\n";

// Loop through all users and display a formatted summary
foreach ($users as $index => $user) {
    // $index is the numeric position (0, 1, 2)
    // $user is the full associative array for that person
    echo "User #" . ($index + 1) . ": " . $user['name'];
    echo " (" . $user['role'] . ")\n";
}

// Add a brand new user to the list
$users[] = [
    'name'  => 'Dave',
    'email' => 'dave@example.com',
    'role'  => 'editor'
];

echo "\nTotal users after adding Dave: " . count($users) . "\n";

// Update a nested value — change Carol's role
$users[2]['role'] = 'admin';
echo "Carol's new role: " . $users[2]['role'] . "\n";
Output
Alice
bob@example.com
User #1: Alice (admin)
User #2: Bob (editor)
User #3: Carol (viewer)
Total users after adding Dave: 4
Carol's new role: admin
Interview Gold: How Deep Can Arrays Nest?
PHP has no hard limit on nesting depth — you can have arrays inside arrays inside arrays. But in practice, if you're going beyond 2-3 levels deep, it's a signal your data structure might need rethinking (or converting to objects). Interviewers love asking this to see if you think about code maintainability, not just whether it works.
Production Insight
Nested arrays from JSON responses can have inconsistent structure — one user might have an extra field, another missing.
A missing inner key will trigger a warning and return NULL, which often propagates silently.
Rule: validate the structure of each nested element before drilling deeper, especially in loops.
Key Takeaway
Multidimensional arrays are lists of structured data.
Access with nested brackets: $arr[outer][inner].
Be prepared for missing keys in nested structures.

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.

array_functions_essentials.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

$temperatures = [22, 18, 31, 15, 27, 9, 34];

// --- count() ---
// Returns the total number of items
echo "Days recorded: " . count($temperatures) . "\n"; // 7

// --- in_array() ---
// Returns true if the value exists somewhere in the array
if (in_array(31, $temperatures)) {\n    echo \"31 degrees was recorded\\n\";\n}\n\n// --- sort() ---\n// Sorts the array in place, lowest to highest\n// WARNING: this MODIFIES the original array — it does NOT return a new one\nsort($temperatures);\necho \"Sorted temperatures: \";\necho implode(', ', $temperatures) . \"\\n\"; // 9, 15, 18, 22, 27, 31, 34\n\n// --- array_filter() ---\n// Keeps only the items where the callback returns true\n// Does NOT modify the original — returns a NEW array\n$hotDays = array_filter($temperatures, function($temp) {\n    return $temp >= 25; // Keep only temperatures 25 and above\n});\necho \"Hot days (25+): \";\necho implode(', ', $hotDays) . \"\\n\"; // 27, 31, 34\n\n// --- array_map() ---\n// Transforms EVERY item using a function — returns a NEW array\n$celsiusTemps = [0, 20, 37, 100];\n$fahrenheitTemps = array_map(function($celsius) {\n    return ($celsius * 9/5) + 32; // Convert Celsius to Fahrenheit\n}, $celsiusTemps);\necho \"Fahrenheit: \";\necho implode(', ', $fahrenheitTemps) . \"\\n\"; // 32, 68, 98.6, 212\n\n// --- array_merge() ---\n// Combines two arrays into one new array\n$morningReadings = [18, 19, 21];\n$afternoonReadings = [28, 31, 29];\n$allReadings = array_merge($morningReadings, $afternoonReadings);\necho \"All readings: \";\necho implode(', ', $allReadings) . \"\\n\"; // 18, 19, 21, 28, 31, 29\n\n// --- array_pop() ---\n// Removes AND returns the last item — modifies the original array\n$lastReading = array_pop($allReadings);\necho \"Removed last reading: \" . $lastReading . \"\\n\"; // 29\necho \"Remaining count: \" . count($allReadings) . \"\\n\"; // 5\n",
        "output": "Days recorded: 7\n31 degrees was recorded\nSorted temperatures: 9, 15, 18, 22, 27, 31, 34\nHot days (25+): 27, 31, 34\nFahrenheit: 32, 68, 98.6, 212\nAll readings: 18, 19, 21, 28, 31, 29\nRemoved last reading: 29\nRemaining count: 5"
      }

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.

FunctionDescriptionReturn ValueModifies Original?
------------
count($arr)Returns the number of elements in an arrayintNo
in_array($needle, $haystack, $strict)Checks if a value exists in an arrayboolNo
array_push($arr, ...$values)Adds one or more elements to the end of an arrayint (new length)Yes
array_pop($arr)Removes and returns the last elementmixedYes
array_merge(...$arrays)Merges one or more arrays into onearrayNo
array_search($needle, $haystack, $strict)Searches array for a given value and returns the first keymixed (key or false)No
array_map($callback, $arr)Applies a callback to every element; returns a new arrayarrayNo
array_filter($arr, $callback)Filters elements via a callback; returns a new arrayarrayNo
array_reduce($arr, $callback, $initial)Iteratively reduces the array to a single value via a callbackmixedNo
sort($arr)Sorts an indexed array in ascending orderbool (true on success)Yes
rsort($arr)Sorts an indexed array in descending orderboolYes
ksort($arr)Sorts an associative array by key in ascending orderboolYes
krsort($arr)Sorts an associative array by key in descending orderboolYes
asort($arr)Sorts an associative array by value in ascending order, preserving keysboolYes
arsort($arr)Sorts an associative array by value in descending order, preserving keysboolYes
array_unique($arr, $flags)Removes duplicate values from an arrayarrayNo (returns new)
array_key_exists($key, $arr)Checks if the specified key exists in the arrayboolNo
array_keys($arr)Returns all the keys of an arrayarrayNo
array_values($arr)Returns all the values of an arrayarrayNo
implode($glue, $pieces)Joins array elements with a string (aliased as join)stringNo

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.

Memory Aid: Functions that Start with 'array_' Usually Return a New Array
There are exceptions — 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.
Production Insight
In production code reviews, the most common array function mistake is forgetting whether the function modifies in place. We flag any line where the return value of sort, rsort, ksort, asort, arsort, array_push, or array_pop is assigned to a variable — that's almost always wrong.
Rule: if you're not sure, check the manual. A 10-second lookup saves a 30-minute debugging session.
Key Takeaway
Keep this table pinned on your desk. Memorise which functions modify the original vs return a new array – it's the single most important distinction to avoid production bugs.

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 list() function (available since PHP 4) and the shorter square bracket syntax (since PHP 7.1). Both do the same thing, but the [] 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: list() only assigns from numeric indices by default. To destructure associative arrays, you must use the [] 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.

array_destructuring.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

// Indexed array destructuring
$fruits = ['apple', 'banana', 'cherry'];

// Using list() — older syntax
list($first, $second, $third) = $fruits;
echo "$first, $second, $third\n"; // apple, banana, cherry

// Using [] syntax (PHP 7.1+) — cleaner
[$first, $second, $third] = $fruits;
echo "$first, $second, $third\n"; // apple, banana, cherry

// Associative array destructuring (PHP 7.1+)
$user = ['name' => 'Sarah', 'email' => 'sarah@example.com', 'age' => 28];
['name' => $name, 'email' => $email] = $user;
echo "Name: $name
Pro Tip: Always Use [] Syntax Over list() for New Code
The [] 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.
Production Insight
In production, destructuring shines when extracting values from database rows or API responses — it eliminates repetitive array access and makes the code read like a declaration.
Rule: use destructuring when you need 2-4 specific values from an array. If the array has more than 4 keys, consider mapping to a value object instead for clarity.
Key Takeaway
Array destructuring with [] or 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, sort() sorts in ascending order, and rsort() 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.

For associative arrays, ksort() sorts by key (ascending) and krsort() by key (descending), preserving key-value associations. asort() sorts by value (ascending) and arsort() 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).

For custom sorting logic, usort() 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.

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);.

array_sorting.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php

// Indexed arrays — sort() and rsort()
$numbers = [3, 1, 4, 1, 5, 9];
sort($numbers); // ascending
echo "Sorted: " . implode(', ', $numbers) . "\n"; // 1, 1, 3, 4, 5, 9

$numbers = [3, 1, 4, 1, 5, 9];
rsort($numbers); // descending
echo "Reverse sorted: " . implode(', ', $numbers) . "\n"; // 9, 5, 4, 3, 1, 1

// Associative arrays — ksort(), asort(), etc.
$inventory = [\n    'apple'  => 5,\n    'banana' => 2,\n    'cherry' => 8\n];

ksort($inventory); // sort by key (alphabetical)
print_r($inventory);
/*
Array
(
    [apple] => 5
    [banana] => 2
    [cherry] => 8
)
*/

$inventory = [
    'apple'  => 5,
    'banana' => 2,
    'cherry' => 8
];
asort($inventory); // sort by value (ascending)
print_r($inventory);
/*
Array
(
    [banana] => 2
    [apple] => 5
    [cherry] => 8
)
*/

// Custom sorting with usort()
$people = [
    ['name' => 'Alice', 'age' => 30],
    ['name' => 'Bob', 'age' => 25],
    ['name' => 'Carol', 'age' => 35]
];

usort($people, function($a, $b) {
    return $a['age'] <=> $b['age']; // spaceship operator: -1, 0, 1
});

foreach ($people as $person) {
    echo $person['name'] . ' is ' . $person['age'] . " years old\n";
}
// Output:
// Bob is 25 years old
// Alice is 30 years old
// Carol is 35 years old

// Preserving original — always copy first
$original = [3, 1, 4];
$sorted = $original;
sort($sorted);
echo "Original: " . implode(', ', $original) . "\n"; // 3, 1, 4
echo "Sorted copy: " . implode(', ', $sorted) . "\n"; // 1, 3, 4
Output
Sorted: 1, 1, 3, 4, 5, 9
Reverse sorted: 9, 5, 4, 3, 1, 1
Array
(\n [apple] => 5\n [banana] => 2\n [cherry] => 8\n)
Array
(
[banana] => 2
[apple] => 5
[cherry] => 8
)
Bob is 25 years old
Alice is 30 years old
Carol is 35 years old
Original: 3, 1, 4
Sorted copy: 1, 3, 4
Sort Functions Always Modify the Original
There is no built-in function to return a sorted copy without affecting the original. If you need to keep the unsorted array for later use, always make a copy first: $copy = $arr; sort($copy);. This is the most common sorting mistake in PHP.
Production Insight
In production, sorting by key is especially common when you need to present data in a specific order (e.g., products sorted by SKU). Always benchmark sorting performance on large datasets — usort with a complex callback can be slow. For stable sorting, consider array_multisort.
Rule: never sort in a loop. Sort once, then iterate.
Key Takeaway
Use 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 array_merge() but with a cleaner syntax and better performance in some cases.

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 array_merge(): 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.

spread_operator_arrays.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

// Basic unpacking into a new array
$parts = [2, 3];
$numbers = [1, ...$parts, 4, 5];
echo implode(', ', $numbers) . "\n"; // 1, 2, 3, 4, 5

// Multiple spreads
$first = ['a', 'b'];
$second = ['c', 'd'];
$combined = [...$first, ...$second, 'e'];
echo implode(', ', $combined) . "\n"; // a, b, c, d, e

// With associative arrays — later keys overwrite earlier ones
$defaults = ['timeout' => 30, 'retries' => 3];
$overrides = ['retries' => 5, 'debug' => true];
$config = [...$defaults, ...$overrides];
print_r($config);
/*
Array
(
    [timeout] => 30
    [retries] => 5
    [debug] => 1
)
*/

// Spread vs array_merge for indexed arrays
$arr1 = [10, 20];
$arr2 = [30, 40];

$merged = array_merge($arr1, $arr2);
print_r($merged); // [0=>10, 1=>20, 2=>30, 3=>40] — re-indexed

$spread = [...$arr1, ...$arr2];
print_r($spread); // [0=>10, 1=>20, 2=>30, 3=>40] — same result for simple indexed arrays

// When arrays have explicit integer keys:
$a = [1 => 'x', 2 => 'y'];
$b = [2 => 'z']; // overlap
$merged = array_merge($a, $b);
print_r($merged); // [0=>'x', 1=>'y', 2=>'z'] — re-indexed all

$spread = [...$a, ...$b];
print_r($spread); // [1=>'x', 2=>'y', 3=>'z'] — preserves original keys, then appends with next available integer
// Note: key 2 from $b conflicts with key 2 from $a, so $b's value 'z' overwrites 'y'? Actually with spread, duplicate integer keys cause overwrite: the later spread source's values win.
// In this case, [...$a, ...$b] means: take $a's [1=>'x', 2=>'y'], then $b's [2=>'z'] overwrites key 2
Output
1, 2, 3, 4, 5
a, b, c, d, e
Array
(\n [timeout] => 30\n [retries] => 5\n [debug] => 1\n)
Array
(
[0] => 10
[1] => 20
[2] => 30
[3] => 40
)
Array
(
[0] => 10
[1] => 20
[2] => 30
[3] => 40
)
Array
(
[0] => x
[1] => y
[2] => z
)
Array
(
[1] => x
[2] => z
)
Pro Tip: Spread Is Faster Than array_merge for Small-to-Medium Arrays
Production Insight
In production, the spread operator is ideal for merging configuration arrays where you want defaults overridden by user settings — it's both readable and efficient.
Rule: use spread for PHP 7.4+ projects; stick with array_merge if you need to support older PHP versions.
Key Takeaway
The spread operator (...) unpacks arrays into a new array literal. It's a cleaner alternative to array_merge for most cases, available from PHP 7.4.

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 arsort(). Then display each student's name and grade.

Exercise 3: Array Deduplication Write a function that removes duplicate values from an array while keeping the first occurrence of each value. Use array_unique(). Test with a mix of integers and strings (with loose comparison issues). Then write a version that uses strict comparison. Compare results.

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 array_filter() with a callback or manually loop. Then output both arrays.

```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 $uniqueStrict = array_unique($data, SORT_STRING); // strict? Actually SORT_STRING still loose. Use array_values(array_unique($data, SORT_REGULAR))? // The correct way for strict: use manual loop or array_flip trick. // Simplified: array_unique with default (SORT_REGULAR) uses loose; to differentiate, you can use array_keys with strict? // For demonstration: echo "Loose unique: "; print_r($uniqueLoose); // [0=>1, 1=>2, 3=>3, 5=>4] (string '2' is considered equal to integer 2, so only first kept) // Strict: use manual: $strictUnique = []; foreach ($data as $value) { if (!in_array($value, $strictUnique, true)) { $strictUnique[] = $value; } } print_r($strictUnique); // [1,2,3,'2',4] — string '2' kept separate

// --- 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) . " "; // 2,4,6 echo "Odd: " . implode(', ', $odd) . " "; // 1,3,5 ```

Try these exercises before reviewing the solution code. They will reinforce the array functions, sorting, destructuring, and traversal patterns covered in this article.

Interview Tip: These Exercises Cover Common Whiteboard Questions
Production Insight
In production, you'll rarely write flatten or partition functions from scratch — they're often one-liners with array_walk_recursive or a simple foreach. But understanding the underlying logic helps you debug when something unexpected happens.
Rule: when a built-in function doesn't exist (like strict unique), write a simple manual loop. It's easier to understand and maintain than a clever but opaque one-liner.
Key Takeaway
Practice these five exercises to solidify your understanding of adding, removing, sorting, deduplicating, flattening, and partitioning arrays — common tasks in every PHP application.

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 unset() on an element inside a 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 array_shift() or array_pop() 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_keys(), then remove them after the loop.

traversal_mutation_pitfalls.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

// Pitfall 1: foreach with &$value reference persists
$items = ['a', 'b', 'c'];
foreach ($items as &$value) {
    $value = strtoupper($value);
}
// $value still references $items[2]!
$value = 'OVERRIDE'; // modifies $items[2] to 'OVERRIDE'
print_r($items); // ['A', 'B', 'OVERRIDE']

// Fix: unset($value) after the loop
unset($value);

// Pitfall 2: unset inside foreach - unexpected results
$arr = [1, 2, 3, 4, 5];
foreach ($arr as $key => $val) {
    if ($val % 2 == 0) {
        unset($arr[$key]);
    }
}
print_r($arr); // [0=>1, 2=>3, 4=>5] - keys are preserved, skipping even numbers works here but can be confusing with numeric reindexing

// Pitfall 3: array_shift inside loop shifts all keys
$queue = ['x', 'y', 'z'];
while ($item = array_shift($queue)) {
    echo $item . ' ';
    // After first iteration, $queue becomes ['y','z'] with keys 0,1
}
echo "\n";
// Works but $queue is now empty - okay for queues but beware of unexpected key renumbering with numeric arrays
Output
Array
(
[0] => A
[1] => B
[2] => OVERRIDE
)
Array
(
[0] => 1
[2] => 3
[4] => 5
)
x y z
Unset $value After foreach With &$value
Leaving a reference variable alive after a foreach is one of the hardest bugs to spot. The variable still points to the last array element. Any assignment to that variable corrupts your array silently. Always unset($value) right after the loop.
Production Insight
I've seen this reference bug corrupt order data in a shopping cart — the last item's price overwrote a subsequent calculation.
The code compiled without error, but the total was wrong for that single order.
Rule: always unset references after foreach, and consider using array_map instead of mutating in place.
Key Takeaway
foreach with &$value creates a reference that persists.
Always unset($value) after the loop.
Prefer immutable transformations (array_map, array_filter) over mutation inside loops.

Type Juggling and Loose Comparison in Arrays

PHP is dynamically typed and famously loose with comparisons. When you use in_array(), array_search(), or array_unique() without strict mode, type coercion can give you false positives or unexpected behaviour.

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, array_unique() uses loose comparison by default, so ['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 array_unique(), use SORT_REGULAR with the flag SORT_STRING or convert to a consistent type first.

type_juggling_arrays.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

// Loose comparison: string '1' matches integer 1
$haystack = [1, 2, 3];
var_dump(in_array('1', $haystack));           // bool(true) - loose
var_dump(in_array('1', $haystack, true));     // bool(false) - strict

// array_unique with loose comparison
$mixed = ['1', 1, 2, '2', 3];
$uniqueLoose = array_unique($mixed);
print_r($uniqueLoose); // [0=>'1', 2=>2, 4=>3] - '1' and 1 merged, '2' and 2 merged

// array_unique with strict flag (SORT_REGULAR is default, but we need SORT_STRING? Actually SORT_REGULAR uses loose too)
// To get strict, use array_unique with SORT_STRING flag
$uniqueStrict = array_unique($mixed, SORT_STRING);
print_r($uniqueStrict); // [0=>'1', 1=>1, 2=>2, 3=>'2', 4=>3] - all kept

// array_keys with loose search
$user = ['name' => 'Sarah', 'role' => '1'];
$keys = array_keys($user, 1, false); // loose: finds 'role' key because '1' == 1
print_r($keys); // [0=>'role']

// Same with strict: $keys = array_keys($user, 1, true); // returns empty because no integer 1 in values
Output
bool(true)
bool(false)
Array
(
[0] => '1'
[2] => 2
[4] => 3
)
Array
(
[0] => '1'
[1] => 1
[2] => 2
[3] => '2'
[4] => 3
)
Array
(
[0] => 'role'
)
Strict Comparison Is Almost Always What You Want
Loose comparison leads to subtle, hard-to-reproduce bugs. A user input of '1' matching an integer key in a whitelist could grant unintended access. Always default to strict (third parameter true) for in_array, array_search, and array_keys. Only use loose when you explicitly need type coercion.
Production Insight
We had a critical bug: an API that checked user role against a whitelist using in_array with loose comparison allowed a user with role string '1' to access admin features.
The role came from a CSV import that stored everything as strings.
Rule: strict comparison is a security best practice, not just a convenience.
Key Takeaway
Loose comparison in array functions can give false positives.
Always pass true as third param to in_array, array_search, array_keys.
Use SORT_STRING flag with array_unique to keep distinct types.
● Production incidentPOST-MORTEMseverity: high

Off-by-One Index Error Took Down the Pricing API at 2 PM

Symptom
Every 10th product in the catalog displayed "Price: N/A" in the storefront. The API returned HTTP 200 but with a null price field.
Assumption
The rookie on call assumed it was a database issue — missing prices in the product table.
Root cause
A developer used $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.
Fix
Replace $items[count($items)] with end($items) or use $items[count($items) - 1]. Add an explicit check for array emptiness before accessing any index.
Key lesson
  • Zero-based indexing is the single most common array bug in PHP.
  • Never assume the last index equals the count — always subtract one.
  • Use end() or array_key_last() for the last element.
Production debug guideSymptom → Action — what to do when array behaviour doesn't match expectations4 entries
Symptom · 01
Accessing array index returns NULL or throws a Warning
Fix
Check that the index exists with array_key_exists() or isset(). Remember: isset() returns false if the value is NULL, while array_key_exists() returns true even for NULL values.
Symptom · 02
Looping over an array with foreach modifies the original unexpectedly
Fix
If you used foreach ($arr as &$value), the reference persists after the loop. Add unset($value) immediately after the loop to break the reference.
Symptom · 03
in_array() returns true for a value that shouldn't be there
Fix
You're probably using loose comparison. Add the third argument true to force strict type comparison: in_array($needle, $haystack, true).
Symptom · 04
sort() doesn't return the sorted array
Fix
sort() modifies the array in place and returns a boolean. Call sort($arr); standalone, then use $arr. To keep the original, copy first: $sorted = $arr; sort($sorted);.
★ PHP Array Debug Cheat SheetQuick reference for the three most common array bugs — spot them in 30 seconds.
Last item returns NULL
Immediate action
Check your index arithmetic: did you use count($arr) instead of count($arr)-1?
Commands
print_r($arr);
echo 'Last index: ' . (count($arr) - 1);
Fix now
Replace count($arr) with count($arr)-1 or use end($arr).
in_array matches wrong type (e.g., '1' matches integer 1)+
Immediate action
Check if you passed `true` as the third argument for strict comparison.
Commands
var_dump(in_array('1', [1, 2, 3], true)); // false
var_dump(in_array('1', [1, 2, 3])); // true with loose
Fix now
Add true as third argument to all in_array() and array_search() calls where types matter.
Foreach loop breaks after first iteration with &$value+
Immediate action
Look for `unset($value)` missing after the loop.
Commands
Check code: foreach ($arr as &$value) { ... } // no unset
Add: unset($value); after the loop.
Fix now
Immediately add unset($value); after every foreach loop that uses reference.
Array Type Comparison
FeatureIndexed ArrayAssociative ArrayMultidimensional Array
Key typeAuto-assigned integers (0, 1, 2...)Custom strings or integers you defineMix of both — outer and inner can differ
Best used forOrdered lists with no named fields (e.g. a list of tags)Structured records with named fields (e.g. user profile)Collections of structured records (e.g. database rows)
Access syntax$tags[0]$user['email']$users[0]['email']
Add new item$tags[] = 'php'$user['phone'] = '555-1234'$users[] = ['name' => 'Eve']
Loop styleforeach ($tags as $tag)foreach ($user as $key => $value)foreach ($users as $user) then access $user['key']
Common real-world useList of category names, image URLsForm POST data, config settings, API response fieldsDatabase result sets, JSON API responses, cart items

Common mistakes to avoid

4 patterns
×

Off-by-one index errors

Symptom
Accessing $items[count($items)] instead of $items[count($items) - 1] to get the last item. A 5-item array has indices 0-4; index 5 returns NULL and triggers a PHP Notice.
Fix
Use $items[count($items) - 1] or the built-in end($items) function which safely returns the last element without needing to know the index.
×

Forgetting that sort() modifies the array in place

Symptom
Beginners write $sorted = sort($prices) expecting a sorted copy in $sorted. Instead, $sorted gets true (the boolean return value), and $prices itself is sorted.
Fix
Call sort($prices) on its own line with no assignment, then read from $prices. If you need to keep the original order intact, copy the array first with $sortedPrices = $prices; sort($sortedPrices);
×

Using == instead of === when searching arrays

Symptom
in_array('1', [1, 2, 3]) returns true with loose comparison because PHP coerces the string '1' to integer 1. This causes phantom matches that are hard to debug.
Fix
Always pass true as the third argument — in_array('1', [1, 2, 3], true) — to enable strict type-safe comparison. It returns false because a string is not strictly equal to an integer.
×

Foreach with &$value without unsetting after the loop

Symptom
After a foreach loop that uses a reference, the $value variable still points to the last array element. Any subsequent assignment to $value will corrupt the array.
Fix
Immediately after the loop, add unset($value); to break the reference. Alternatively, avoid references by using array_map or copying the array.
🔥

That's PHP Basics. Mark it forged?

13 min read · try the examples if you haven't

Previous
PHP Functions
6 / 14 · PHP Basics
Next
PHP Strings and String Functions