One Type Doing Two Jobs
Many languages have separate types for a list and a dictionary. PHP has one type that does both. A PHP array is really an ordered map: a set of key-to-value pairs that remembers the order you added them in. When the keys happen to be 0, 1, 2, 3 you call it an indexed array; when they are words you call it associative; but underneath it is the same structure.
That unification is what makes PHP arrays so pleasant to work with, and it explains a few behaviours that otherwise look arbitrary. Keys can only be integers or strings. If you use anything else, PHP converts it: the string "8" becomes the integer 8, true becomes 1, false becomes 0, null becomes the empty string, and a float is truncated toward zero. So $a["8"] and $a[8] are the same slot — though $a["08"] is a different one, because "08" is not a canonical integer string.
Because order is preserved separately from the keys, an array can have gaps and can be out of order without anything breaking. This matters more than it sounds, and the next section shows exactly where it surprises people.
In real projects you will meet arrays mostly in three shapes: a simple list of values, an associative array representing one record, and a list of associative arrays representing many records. That last shape is what every database query returns, and getting comfortable with it is most of what this lesson is for.
<?php
// A list
$subjects = ['Physics', 'Chemistry', 'Maths'];
echo $subjects[0]; // Physics
// One record
$student = [
'id' => 118,
'name' => 'Ananya Sharma',
'email' => 'ananya@example.com',
'active' => true,
];
echo $student['name'];
// Many records - what a database query gives you
$results = [
['name' => 'Ananya', 'marks' => 91],
['name' => 'Ravi', 'marks' => 78],
];
echo $results[1]['name']; // Ravi
// Key conversion in action
$a = [];
$a["8"] = 'eight';
$a[8] = 'overwritten'; // same slot as "8"
$a["08"] = 'zero-eight'; // a DIFFERENT key - not a canonical integer
$a[true] = 'one'; // becomes key 1
var_dump(array_keys($a)); // [8, "08", 1]
// Nested structure, read from the outside in
$college = [
'name' => 'City College',
'courses' => [
'CSE' => ['seats' => 60, 'hod' => 'Dr Rao'],
'ECE' => ['seats' => 45, 'hod' => 'Dr Nair'],
],
];
echo $college['courses']['CSE']['seats']; // 60 print_r($arr)prints a readable tree of any array and is the fastest way to see what you actually have.var_dump($arr)shows types and lengths too. Wrap either in<pre>tags when debugging in a browser, or the whole structure collapses onto one line.
Adding, Removing, and the Gap unset Leaves Behind
Appending to an array is $arr[] = $value;. There is also array_push(), which does the same thing and can add several values at once, but for a single value the bracket form is shorter and idiomatic. Note what PHP chooses as the new key: one more than the highest integer key ever used, not the current element count.
That distinction becomes visible the moment you remove something. unset($arr[1]) deletes the element but does not renumber the rest, so you are left with keys 0, 2, 3 and a hole where 1 used to be. The array still has three elements, and count() correctly says 3 — but a for loop counting 0, 1, 2 will now hit a missing key and warn.
This is one of the most common sources of "Undefined array key" warnings after a delete operation. The fix is array_values(), which throws away the keys and renumbers from zero. Call it whenever you have removed items from a list and something downstream expects a clean sequence — most importantly before json_encode(), for the reason the filtering section explains.
For removing from the ends, array_pop() takes the last element off and array_shift() takes the first, both returning what they removed. array_shift() reindexes the remaining numeric keys, and on a large array it has to touch every element, so avoid it inside a loop over a big list.
<?php
$tags = ['php', 'mysql', 'security'];
$tags[] = 'forms'; // append; key 3
array_push($tags, 'pdo', 'oop'); // append several
// Removing leaves a gap
$list = ['a', 'b', 'c', 'd'];
unset($list[1]);
print_r($list);
// [0 => 'a', 2 => 'c', 3 => 'd'] <- no key 1
count($list); // 3
// This now warns on $i = 1
// for ($i = 0; $i < count($list); $i++) { echo $list[$i]; }
// Renumber
$list = array_values($list);
print_r($list); // [0 => 'a', 1 => 'c', 2 => 'd']
// The append key is "highest ever + 1", not "count"
$x = [];
$x[10] = 'ten';
$x[] = 'next';
var_dump(array_keys($x)); // [10, 11]
// Ends
$last = array_pop($tags); // removes and returns the last
$first = array_shift($tags); // removes and returns the first, reindexes
array_unshift($tags, 'intro');// add to the front
// Useful checks
echo count($tags);
var_dump($tags === []); // is it empty?
echo array_key_first($x); // 10
echo array_key_last($x); // 11 - PHP 8.1 added
array_is_list(), which returns true only when the keys are exactly 0, 1, 2, … with no gaps. It is the precise way to ask "is this still a clean list?" before handing an array to something that assumes one.
map, filter and reduce
These three functions replace most of the loops you would otherwise write. array_map() applies a function to every element and returns a new array of the same size. array_filter() keeps only the elements for which your test returns true. array_reduce() boils an array down to a single value — a total, a maximum, a joined string.
The advantage is not brevity but clarity of intent. A reader sees array_filter and knows immediately that the result is a subset with the same elements; a hand-written loop could be doing anything and has to be read line by line. Arrow functions, written fn($x) => ..., make them compact enough to fit on one line.
Now the gotcha, and it is a real one: array_filter() preserves the original keys. Filtering [0=>'a', 1=>'b', 2=>'c'] down to just the last element gives you [2=>'c'] — an array whose only key is 2. That is usually harmless inside PHP, and disastrous the moment you call json_encode() on it, because a PHP array with non-sequential keys encodes as a JSON object, not an array. Your JavaScript then receives {"2":"c"} where it expected ["c"], and .map() on the other side fails.
The fix is always the same: wrap the result in array_values(). Make it a reflex — any time a filtered array is about to become JSON, or is about to be looped over by index, reindex it first.
One more difference worth knowing: array_filter() passes the value to your callback by default, not the key. If you need to filter on keys, pass ARRAY_FILTER_USE_KEY as the third argument, or ARRAY_FILTER_USE_BOTH to receive both. And calling array_filter() with no callback at all removes every falsy value, which is a quick way to clean a list — remembering that it also removes legitimate zeros.
<?php
$marks = [91, 45, 78, 32, 88];
// map: same size, transformed
$percent = array_map(fn($m) => $m . '%', $marks);
// filter: subset, ORIGINAL KEYS PRESERVED
$passed = array_filter($marks, fn($m) => $m >= 40);
print_r($passed);
// [0 => 91, 2 => 78, 4 => 88] <- keys 1 and 3 are gone
echo json_encode($passed);
// {"0":91,"2":78,"4":88} <- a JSON object, probably not what you want
echo json_encode(array_values($passed));
// [91,78,88] <- a JSON array
// reduce: one value out
$total = array_reduce($marks, fn($carry, $m) => $carry + $m, 0);
$highest = array_reduce($marks, fn($c, $m) => max($c, $m), 0);
echo $total; // 334
// For plain sums, the dedicated function is clearer
echo array_sum($marks); // 334
echo round(array_sum($marks) / count($marks), 1); // 66.8
// Filtering on keys
$config = ['db_host' => 'localhost', 'db_user' => 'root', 'app_name' => 'Portal'];
$dbOnly = array_filter(
$config,
fn($key) => str_starts_with($key, 'db_'),
ARRAY_FILTER_USE_KEY
);
// No callback: drops every falsy value
print_r(array_filter(['a', '', 'b', null, 0, 'c']));
// [0 => 'a', 2 => 'b', 5 => 'c'] - note the 0 was removed too array_map()keeps string keys when called with a single array, but renumbers when called with several arrays at once. If keys matter to you, check what you actually got withprint_r()rather than assuming.
Sorting Without Losing Your Data
PHP's sort functions come as a family, and the names encode what they do. Plain sort() sorts by value and throws the keys away, renumbering from zero. asort() sorts by value but keeps each value attached to its key. ksort() sorts by key. Add r for reverse — rsort(), arsort(), krsort() — and u for a user-supplied comparison: usort(), uasort(), uksort().
The trap is in the first one. Running sort() on an associative array of names to marks destroys every name and leaves you with a bare list of numbers. If your data has meaningful keys, you almost always want asort() or arsort().
The second trap is that these functions sort in place and return a boolean, not the sorted array. Writing $sorted = sort($marks); puts true into $sorted and quietly modifies $marks. It is a mistake that produces a very confusing var_dump the first time you make it.
For a list of records — the shape every database query returns — you need usort() with a comparison function. The spaceship operator makes it a one-liner, and chaining two comparisons with ?: handles ties. Remember usort() reindexes; use uasort() if the outer keys matter.
One last practical point: sorting strings compares them byte by byte, so uppercase letters come before lowercase ones and "Zebra" sorts before "apple". For a user-facing list of names, either normalise the case in your comparison or use SORT_NATURAL | SORT_FLAG_CASE — and for a list that is coming out of a database anyway, the cleanest answer is to let SQL do the sorting with ORDER BY.
<?php
$marks = ['Ananya' => 91, 'Ravi' => 78, 'Meera' => 88];
// WRONG - loses the names
sort($marks);
print_r($marks); // [0 => 78, 1 => 88, 2 => 91]
// RIGHT - keeps the association
$marks = ['Ananya' => 91, 'Ravi' => 78, 'Meera' => 88];
arsort($marks); // by value, highest first
print_r($marks); // ['Ananya' => 91, 'Meera' => 88, 'Ravi' => 78]
ksort($marks); // alphabetical by name
// They return a boolean, not the array
$result = sort($marks);
var_dump($result); // bool(true) <- not the sorted array
// Sorting records
$students = [
['name' => 'Ravi', 'marks' => 78],
['name' => 'Ananya', 'marks' => 91],
['name' => 'Meera', 'marks' => 78],
];
usort($students, fn($a, $b) =>
($b['marks'] <=> $a['marks']) ?: strcmp($a['name'], $b['name'])
);
// Ananya 91, Meera 78, Ravi 78
// Case-insensitive, human-friendly string sort
$files = ['item10.txt', 'Item2.txt', 'item1.txt'];
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
// item1.txt, Item2.txt, item10.txt - If the data came from a database, sort it in SQL with
ORDER BYrather than in PHP. The database can use an index to do it, it does not have to load every row into memory first, and it works correctly across pages when you addLIMIT. Sorting in PHP after aLIMITonly sorts the page you already fetched, which produces subtly wrong results.
Searching, Combining and Reshaping
in_array() answers whether a value is present and array_search() returns the key where it was found. Both use loose comparison unless you pass true as the third argument, and both deserve that flag by default for the reasons the type-juggling lesson covered. Remember also that array_search() returns the key — which can be 0 — so test its result with !== false.
Combining arrays has two operators with genuinely different behaviour, and picking the wrong one is a quiet bug. array_merge() appends and renumbers integer keys, so two lists of three become one list of six; for string keys, later values overwrite earlier ones. The + operator does the opposite: it keeps the left side's value whenever a key exists in both, and it does not renumber anything.
That makes + the right tool for applying defaults — $options = $userOptions + $defaults; reads as "use what the user gave, fall back to the defaults" — and array_merge() the right tool for concatenating lists. Using array_merge() for defaults works too, with the arguments the other way round, but on numerically indexed data it will renumber and surprise you.
array_column() is the one function in this section that will change how you write code. Given a list of records, it pulls out one field as a flat list — and with a third argument it uses another field as the key, turning a list into a lookup table indexed by id. That single call is what makes the fix for the N+1 query problem from the loops lesson so short.
Finally, destructuring lets you pull values out by position or by key in one line, and the spread operator ... unpacks one array into another. Both make code that assembles and takes apart records considerably easier to read.
<?php
$tags = ['php', 'mysql', 'security'];
var_dump(in_array('php', $tags, true)); // true - strict
$pos = array_search('mysql', $tags, true);
if ($pos !== false) { echo "found at $pos"; } // 1
// merge vs + : different answers
$a = ['x', 'y'];
$b = ['p', 'q'];
print_r(array_merge($a, $b)); // ['x','y','p','q'] - renumbered
print_r($a + $b); // ['x','y'] - keys 0,1 already taken
$defaults = ['perPage' => 20, 'sort' => 'name', 'dir' => 'asc'];
$user = ['perPage' => 50];
print_r($user + $defaults);
// ['perPage' => 50, 'sort' => 'name', 'dir' => 'asc']
// array_column: the workhorse for database results
$rows = [
['id' => 7, 'name' => 'Ananya', 'marks' => 91],
['id' => 12, 'name' => 'Ravi', 'marks' => 78],
];
print_r(array_column($rows, 'name'));
// ['Ananya', 'Ravi']
print_r(array_column($rows, 'name', 'id'));
// [7 => 'Ananya', 12 => 'Ravi'] - an instant lookup table
print_r(array_column($rows, null, 'id'));
// whole rows, keyed by id
// Other regulars
print_r(array_unique([1, 2, 2, 3, 3])); // [0=>1, 1=>2, 3=>3] - keys kept
print_r(array_slice($rows, 0, 1)); // first record only
print_r(array_keys(['a' => 1, 'b' => 2])); // ['a','b']
print_r(array_flip(['a' => 1, 'b' => 2])); // [1 => 'a', 2 => 'b']
// Destructuring and spread
['name' => $name, 'marks' => $m] = $rows[0];
[$first, $second] = $tags;
$all = [...$tags, 'forms', 'sessions']; - Nested destructuring works too:
foreach ($rows as ['id' => $id, 'name' => $name]). If a key you name is missing from a record, you get a warning andnull, so only destructure keys you are confident are always present.
