PHP Multidimensional Arrays
A multidimensional array is simply an array whose values are themselves arrays. PHP does not have a dedicated “matrix” or “table” type — instead you nest ordinary arrays inside ordinary arrays, as deeply as you like, to model grids, records, trees, and other structured data. Once you understand that a multidimensional array is just “arrays all the way down,” the rest is a matter of indexing correctly and looping at the right depth.
Overview / How It Works
Every PHP array, whether indexed or associative, is internally an ordered hash map. Each slot (bucket) stores a key and a value, and that value can be a scalar (int, string, float, bool), null, an object, or — critically for this lesson — another array. There is no special engine support for “2D arrays” or “3D arrays”; a two-dimensional array is just an array whose every element happens to be an array, and a three-dimensional array is an array of arrays of arrays. The nesting can be irregular: one row can have three columns while another has five, and you can even mix indexed and associative sub-arrays freely, because PHP arrays don’t enforce a fixed shape.
Multidimensional arrays commonly appear in two flavors. The first is the numeric grid, useful for things like a tic-tac-toe board, a spreadsheet-style table, or coordinates: $grid[$row][$col]. The second, far more common in real applications, is the array of records, where each element is an associative array representing one “row” of data — a user, a product, an order — and the outer array is the list or the table. You will also frequently see associative arrays nested inside associative arrays, for example a configuration tree like $config['database']['host'], which mirrors how you might store JSON or a YAML file’s structure once decoded with json_decode($json, true).
Under the hood, each nested array is its own independent hash table (its own zval container). PHP uses copy-on-write: when you assign an array to another variable, or pass it to a function by value, PHP does not immediately duplicate the underlying memory. Both variables point to the same data until one of them is modified, at which point PHP copies the structure so the two variables can diverge safely. This matters enormously for multidimensional arrays because it means every level of nesting is copied independently the moment it changes — which is also the root cause of one of the most common bugs, covered in Common Mistakes below.
Syntax
There is no special syntax for “multidimensional” — you use the normal array literal syntax and simply put an array literal where a value would normally go:
<?php
$matrix = [
[1, 2, 3],
[4, 5, 6],
];
$user = [
"name" => "Grace",
"address" => [
"city" => "Austin",
"zip" => "73301",
],
];
| Part | Description |
|---|---|
[ ] |
Short array literal syntax (works for any nesting depth) |
| Outer key | Selects a row or sub-array from the outer array, e.g. $matrix[0] |
| Inner key | Selects a value inside the chosen sub-array, e.g. $matrix[0][2] |
=> |
Assigns an explicit key (int or string) to a value in an associative array |
| Chained brackets | Each pair of brackets descends one level: $user['address']['city'] |
You read or write a nested value by chaining index operators from the outside in: $matrix[1][2] reads row 1, column 2 (value 6), and $user['address']['city'] reads "Austin". You can chain as many levels as your data needs — PHP has no built-in depth limit beyond available memory.
Examples
Example 1: A simple grade table
<?php
$grades = [
["Alice", 92, 88, 95],
["Bob", 78, 85, 80],
["Charlie", 90, 91, 89],
];
foreach ($grades as $student) {
$name = $student[0];
$average = ($student[1] + $student[2] + $student[3]) / 3;
echo "$name: " . round($average, 2) . "\n";
}
Output:
Alice: 91.67
Bob: 81
Charlie: 90
Here the outer array is a plain indexed list of “rows,” and each row is itself an indexed array where position 0 is the name and positions 1–3 are scores. The outer foreach gives us one row ($student) at a time, and we index into that row to pull out individual fields.
Example 2: An associative array of records
<?php
$inventory = [
"laptop" => ["price" => 999.99, "stock" => 12, "tags" => ["electronics", "computers"]],
"mouse" => ["price" => 19.99, "stock" => 150, "tags" => ["electronics", "accessories"]],
];
foreach ($inventory as $product => $details) {
$tagList = implode(", ", $details["tags"]);
printf("%s costs $%.2f, %d in stock. Tags: %s\n", ucfirst($product), $details["price"], $details["stock"], $tagList);
}
Output:
Laptop costs $999.99, 12 in stock. Tags: electronics, computers
Mouse costs $19.99, 150 in stock. Tags: electronics, accessories
This is the shape you’ll meet constantly in real applications: the outer array’s keys are meaningful identifiers (product names), and each value is an associative “record” that itself contains a nested indexed array (tags). The foreach ($inventory as $product => $details) syntax destructures both the key and the value in one step.
Example 3: Three levels deep, with a write-back
<?php
$company = [
"Engineering" => [
"Backend" => ["Dana", "Omar"],
"Frontend" => ["Priya", "Wei"],
],
"Sales" => [
"Enterprise" => ["Luis"],
],
];
foreach ($company as $department => $teams) {
echo "$department:\n";
foreach ($teams as $team => $members) {
echo " $team (" . count($members) . "): " . implode(", ", $members) . "\n";
}
}
$company["Engineering"]["Backend"][] = "Sam";
echo "New backend count: " . count($company["Engineering"]["Backend"]) . "\n";
Output:
Engineering:
Backend (2): Dana, Omar
Frontend (2): Priya, Wei
Sales:
Enterprise (1): Luis
New backend count: 3
This models a three-level tree: department → team → list of employee names. Traversing it needs one nested foreach per level. The final two lines show that you can write directly to a deeply nested location — $company["Engineering"]["Backend"][] = "Sam" — by chaining index operators all the way down and using the empty-bracket append syntax on the innermost array.
How It Works Step by Step
- When PHP parses
$matrix[1][2], it evaluates left to right: first it looks up key1in$matrix‘s hash table, which returns a reference to another array. Then it looks up key2in that array’s hash table. - Each level of nesting is a completely separate hash table with its own set of buckets; there is no shared “matrix memory” the way there would be in a language with true multidimensional array types.
- Because of copy-on-write, assigning
$copy = $matrixdoes not copy any memory yet. As soon as you modify$copy[0][0], PHP copies the outer array structure (and only the sub-array that was actually touched needs to be duplicated) so that$matrixis left untouched. - Passing a multidimensional array into a function without
&passes it by value under the same copy-on-write rules — the function receives what looks like an independent copy, and any mutation inside the function does not affect the caller’s array. - Appending with
[]at the innermost level ($arr[$a][$b][] = $value) works because PHP resolves$arr[$a][$b]down to the target array first, then performs the append on that specific sub-array.
Common Mistakes
Mistake 1: Expecting a function to modify the caller’s array without a reference
Because arrays are passed by value by default, modifying a multidimensional array parameter inside a function has no effect on the original:
<?php
function addBonus($employees) {
foreach ($employees as $dept => $list) {
$employees[$dept][] = "Bonus Employee";
}
}
$staff = ["HR" => ["Ann"], "IT" => ["Ben"]];
addBonus($staff);
print_r($staff);
Output:
Array
(
[HR] => Array
(
[0] => Ann
)
[IT] => Array
(
[0] => Ben
)
)
$staff is unchanged — $employees inside the function was an independent copy. Fix it by passing the parameter by reference, or by returning the modified array and reassigning it:
<?php
function addBonus(array &$employees): void {
foreach ($employees as $dept => $list) {
$employees[$dept][] = "Bonus Employee";
}
}
$staff = ["HR" => ["Ann"], "IT" => ["Ben"]];
addBonus($staff);
print_r($staff);
Output:
Array
(
[HR] => Array
(
[0] => Ann
[1] => Bonus Employee
)
[IT] => Array
(
[0] => Ben
[1] => Bonus Employee
)
)
Mistake 2: Modifying a foreach value without &
The same value-vs-reference confusion shows up inside loops. foreach ($arr as $row) gives you a copy of each row, so changing $row never touches the original array:
<?php
$scores = [
["math" => 70, "science" => 80],
["math" => 90, "science" => 60],
];
foreach ($scores as $row) {
$row["math"] = $row["math"] + 10;
}
print_r($scores);
Output:
Array
(
[0] => Array
(
[math] => 70
[science] => 80
)
[1] => Array
(
[math] => 90
[science] => 60
)
)
Nothing changed, because $row was a local copy. To mutate the original array in place, loop by reference, and always unset() the reference variable afterward to avoid it accidentally aliasing a later variable of the same name:
<?php
$scores = [
["math" => 70, "science" => 80],
["math" => 90, "science" => 60],
];
foreach ($scores as &$row) {
$row["math"] = $row["math"] + 10;
}
unset($row);
print_r($scores);
Output:
Array
(
[0] => Array
(
[math] => 80
[science] => 80
)
[1] => Array
(
[math] => 100
[science] => 60
)
)
Best Practices
- Use
&(reference) inforeachonly when you genuinely need to mutate the array in place, and alwaysunset()the reference variable right after the loop. - Prefer
$row['key'] ?? $default(the null coalescing operator) instead of raw index access when a nested key might not exist, to avoid “undefined array key” warnings. - Keep sub-array “shape” consistent (the same keys in every row) when representing tabular data — it makes downstream code far more predictable and lets you use functions like
array_column(). - Use
array_column($records, 'price')to pull one field out of every row of a multidimensional array instead of writing a manual loop. - Use
array_map(),array_filter(), andarray_walk_recursive()for transforming or scanning nested structures instead of deeply nested manual loops when the logic is simple. - For deeply nested or truly dynamic depth structures (trees, JSON-like config), consider a recursive function rather than hardcoding a fixed number of nested loops.
- Use
print_r()orvar_dump()while debugging to see the exact shape of a multidimensional array — guessing the structure is a common source of bugs. - When decoding JSON into arrays, pass
trueas the second argument tojson_decode()(json_decode($json, true)) to get nested associative arrays instead of nested objects, if array syntax is what your code expects.
Practice Exercises
- Exercise 1: Given
$temperatures = ["Mon" => [61, 66, 59], "Tue" => [64, 70, 62]](each value is[low, high, average]), write a loop that prints each day’s high temperature. Expected output includes two lines, one per day. - Exercise 2: Write a function
totalStock(array $inventory): intthat takes an associative array of products (each with astockkey, as in Example 2) and returns the sum of all stock counts. Test it against a small inventory array. - Exercise 3: Starting from the
$companyarray in Example 3, write code that counts the total number of employees across every department and team (hint: you’ll need two nested loops and a running counter, orarray_walk_recursive()).
Summary
- A multidimensional array is just an array whose values are themselves arrays — PHP has no separate “matrix” type.
- Access nested values by chaining index operators from outside in, e.g.
$arr['a']['b'][0]. - Use nested
foreachloops to traverse each level; destructure keys and values withas $key => $value. - Arrays are copied by value on write (copy-on-write) — both function parameters and
foreachloop variables are copies unless you explicitly use&. - Always
unset()a reference variable created byforeach (... as &$x)right after the loop to prevent subtle aliasing bugs. - Use the null coalescing operator (
??) when reading keys that might not exist in a nested structure. - Helper functions like
array_column(),array_map(), andarray_walk_recursive()often replace manual nested loops for common tasks.
