Arrays — PHP's Swiss Army Knife
The two faces — list and dict in one type
Here's what makes PHP arrays unusual. You can use the same syntax to make a list (with numbered positions) or a dict (with string keys), and the same set of operations works on both. Under the hood it's all one type — indexed arrays just happen to use 0, 1, 2 as their automatic keys.
// Indexed (zero-based list)
$fruits = ['apple', 'banana', 'cherry'];
$fruits[0]; // 'apple'
// Associative (string keys)
$user = ['id' => 1, 'name' => 'Eric', 'admin' => true];
$user['name']; // 'Eric'
🐍 Python: PHP arrays = a Python list and dict mashed into one type. Indexed ≈ list. Associative ≈ dict. PHP uses => for key/value pairs where Python uses :. Iteration order is preserved in both languages (Python 3.7+ guaranteed; PHP always).
Add, remove, look up
The core operations every array needs. PHP has dedicated functions for all of them, but the most common ones have shorter syntax:
$fruits[] = 'date'; // append to the end
array_push($fruits, 'elderberry'); // same thing
$last = array_pop($fruits); // remove + return last
$first = array_shift($fruits); // remove + return first
array_unshift($fruits, 'apricot'); // prepend to the start
unset($user['admin']); // remove a key
isset($user['name']); // does this key exist AND not null?
array_key_exists('name', $user); // does the key exist, even if null?
That last pair is a subtle but important distinction. isset returns true if the key exists AND its value isn't null. array_key_exists returns true if the key is there, regardless of what's in it. When does this matter? When a key's value can legitimately be null. With isset, "key not present" and "key present but null" look identical. With array_key_exists, you can tell them apart. For most everyday code, isset is what you want. Reach for array_key_exists when null is a meaningful state in your data.
Iterating
Two main patterns. The first gives you just the values (useful for lists), the second gives you both keys and values (useful for associative arrays):
foreach ($fruits as $f) {
echo $f, "\n";
}
foreach ($user as $key => $value) {
echo "$key = $value\n";
}
PHP figures out which form to use based on the syntax. Both work on either kind of array — you can foreach an indexed array with key/value and you'll get 0, 1, 2 as the keys.
The "you'll use these every day" functions
PHP's array function library is enormous — over 80 functions. Don't try to learn them all; learn the ones you'll reach for constantly:
// Search
in_array('apple', $fruits); // true / false — is value present?
array_search('apple', $fruits); // index or false
// Slice and dice
array_slice($fruits, 1, 2); // sub-array from index 1, length 2
array_reverse($fruits);
array_merge($a, $b); // concatenate (numeric keys reindex)
[...$a, ...$b] // same thing, spread syntax (PHP 7.4+)
// Keys and values
array_keys($user); // ['id', 'name', 'admin']
array_values($user); // [1, 'Eric', true]
// Counting
count($fruits);
array_count_values(['a', 'b', 'a']); // ['a' => 2, 'b' => 1]
// Filter, map, reduce — the functional trio
$adults = array_filter($users, fn($u) => $u['age'] >= 18);
$names = array_map(fn($u) => $u['name'], $users);
$total = array_reduce($items, fn($acc, $i) => $acc + $i['price'], 0);
// Sorting
sort($fruits); // by value, reindex (loses keys)
asort($fruits); // by value, KEEP keys
ksort($user); // by key
usort($users, fn($a, $b) => $a['age'] <=> $b['age']); // custom comparison
That weird-looking <=> on the last line is called the "spaceship operator" (because it looks like a tiny ship). It returns -1, 0, or 1 depending on whether the left side is less than, equal to, or greater than the right. That's exactly what usort wants for sorting. Fun name, useful operator.
🐍 Python: array_filter ≈ list(filter(fn, arr)). array_map ≈ list(map(fn, arr)). array_reduce ≈ functools.reduce. PHP returns arrays directly; Python returns iterators (you wrap in list()). Same idea, slightly different return shape.
Destructuring — unpack into variables
You can unpack array elements directly into named variables. Useful when a function returns multiple values, or when you're working with a known structure:
// Indexed
[$first, $second, $third] = $fruits;
// Associative (PHP 7.1+)
['name' => $name, 'id' => $id] = $user;
// Skip elements
[, , $third] = $fruits;
The associative version is the chef's-kiss move. Look how much cleaner this is:
// The painful old way
$name = $user['name'];
$id = $user['id'];
$admin = $user['admin'];
// The lovely new way
['name' => $name, 'id' => $id, 'admin' => $admin] = $user;
When you've got more than two or three fields, the difference becomes huge. Pattern matching vibes.
Nested arrays
Arrays can contain other arrays, as deep as you need. This is how most real-world data ends up shaped — a list of records, where each record is an associative array:
$users = [
['id' => 1, 'name' => 'Eric', 'tags' => ['admin','editor']],
['id' => 2, 'name' => 'Aiko', 'tags' => ['editor']],
];
echo $users[0]['name']; // "Eric"
echo $users[0]['tags'][1]; // "editor"
foreach ($users as $user) {
echo $user['name'], ': ',
implode(', ', $user['tags']),
"\n";
}
Two new functions in there worth memorizing — they come up every single day:
implode($separator, $array)— joins array elements into a single string with the separator between each.explode($separator, $string)— the reverse: splits a string into an array on the separator.
$csv = "a,b,c";
$parts = explode(',', $csv); // ['a', 'b', 'c']
Remember the pair as "implode joins, explode splits." Burn it in.
Arrays copy on assign — the gotcha
Here's a behavior that surprises a lot of people. When you assign one array variable to another, you get a COPY, not a reference:
$a = [1, 2, 3];
$b = $a; // COPY, not a reference
$b[] = 4;
print_r($a); // still [1, 2, 3]
print_r($b); // [1, 2, 3, 4]
Same thing when you pass an array into a function — the function gets a copy. Mutations inside the function don't affect the caller's original array unless you explicitly pass by reference (with &) or return the modified array.
Heads up though: this is the OPPOSITE of how objects work in PHP. Objects are reference-by-default. So $obj2 = $obj1 means they both point at the same object. The asymmetry between arrays and objects is a classic PHP quirk worth filing away mentally — it does sometimes feel like "why are these different?" The answer is "history." Just remember it.
🐍 Python: Opposite default! Python b = a shares the list (both point at the same object) — you'd need b = a.copy() or b = a[:] to get PHP's behavior. PHP's "array = copy, object = reference" split is unique to PHP. Just file it: array copies, object shares.
When NOT to use an array
If you find yourself with an array of associative arrays that all have the same shape — like the $users example above where every element has 'id', 'name', 'tags' — that's a sign you should make a real class instead. Why? Because:
- The compiler can catch typos in field names. Type
$user['nme']and PHP silently returns null. Type$user->nmeon an object and you get an error. - Type hints work. You can declare a function takes a User and PHP enforces it.
- IDE autocomplete works. Type
$user->and the IDE shows you what fields exist. - Methods can live on the object — behavior lives with data instead of being scattered.
We'll cover classes properly in chapter 10. For now, just know that "consistent-shape associative arrays" is a code smell that says "this wants to be a class."
Build: Sortable Med List
Time to extend the meds page from chapter 3 with sorting, summary counts, and a few more meds. Same patterns you've used before, plus the new array functions from this chapter. The page will end up feeling noticeably more "real."
- Open your existing
meds.php. - Add more meds to the array — aim for at least eight, mix of statuses. The variety makes the sorting visible.
- After the meds array, add a sort step and a counts step:
// Sort $sort = $_GET['sort'] ?? 'name'; usort($meds, fn($a, $b) => match($sort) { 'dose' => $b['dose'] <=> $a['dose'], // dose desc 'status' => $a['status'] <=> $b['status'], default => $a['name'] <=> $b['name'], // name asc }); // Count per status $counts = array_count_values(array_column($meds, 'status')); $total = count($meds); - Above the filter chips, add a summary line:
<div style="margin-bottom:10px;color:#b9adcf"> Total: <?= $total ?> | Taken: <?= $counts['taken'] ?? 0 ?> | Due: <?= $counts['due'] ?? 0 ?> | Overdue: <?= $counts['overdue'] ?? 0 ?> </div> - Add sort chips next to your filter chips:
<div class="filters"> Sort: <a href="?sort=name">Name</a> <a href="?sort=dose">Dose</a> <a href="?sort=status">Status</a> </div> - Visit
http://192.168.0.19/meds.php?sort=dose. Notice the order change. Try each sort option.
Stretch goals:
- Combine sort + filter in the same URL (e.g.
?sort=dose&filter=overdue) and make both apply together. - Use
array_filterto filter BEFORE displaying instead of usingcontinueinside the loop. Cleaner separation of "fetch+filter" from "render." - Compute total dose across all meds using
array_reduce. Display it in the summary.
What you flexed: usort with the spaceship operator, match inside a closure (advanced!), array_count_values + array_column, the null coalescing operator in templates for safe counts, the pattern of "sort, filter, count, render" as separate steps. This is starting to look like a real list view.