Multidimensional Arrays
What Is a Multidimensional Array?
In simple terms: A multidimensional array is an array that contains other arrays. It lets you store data with more than one level — like a table with rows and columns, or a list of records where each record has several fields.
Technically: PHP arrays can hold values of any type, including arrays. When elements are themselves arrays, the structure becomes nested. Each level is accessed with its own pair of square brackets: $data[$row][$column]. There is no fixed limit on depth.
Why They Matter
Real data is rarely a flat list:
- Tables: rows from a database, each with several columns
- Grids: seat maps, game boards, spreadsheets
- Nested records: an order with a customer and a list of items
- Configuration: settings grouped by section
- API responses: JSON documents with nested objects and lists
Three Common Shapes
1. A Grid (List of Lists)
<?php
$matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
echo $matrix[1][2] . "\n"; // row 1, column 2
foreach ($matrix as $row) {
echo implode(' ', $row) . "\n";
}
$diagonalSum = 0;
for ($i = 0; $i < count($matrix); $i++) {
$diagonalSum += $matrix[$i][$i];
}
echo "Diagonal sum: $diagonalSum\n";
Output
6
1 2 3
4 5 6
7 8 9
Diagonal sum: 15
$matrix[1]is the second row ([4, 5, 6]);[2]selects its third element (6).- The diagonal elements are where row and column index are equal:
[0][0],[1][1],[2][2]→1 + 5 + 9 = 15.
2. A Table of Records (List of Associative Arrays)
This is the most common shape in web applications — it is what a database query returns:
<?php
$users = [
['id' => 1, 'name' => 'Asha', 'city' => 'Chennai'],
['id' => 2, 'name' => 'Bala', 'city' => 'Madurai'],
['id' => 3, 'name' => 'Charu', 'city' => 'Coimbatore'],
];
echo $users[1]['name'] . "\n";
foreach ($users as $user) {
echo "#{$user['id']} {$user['name']} — {$user['city']}\n";
}
Output
Bala
#1 Asha — Chennai
#2 Bala — Madurai
#3 Charu — Coimbatore
Each element of $users is a record; inside the loop, $user['name'] reads one field.
3. A Tree (Nested Associative Arrays)
<?php
$config = [
'app' => ['name' => 'GoCourse', 'debug' => false],
'database' => [
'default' => 'mysql',
'connections' => [
'mysql' => ['host' => 'localhost', 'port' => 3306],
'sqlite' => ['path' => '/data/app.sqlite'],
],
],
];
$driver = $config['database']['default'];
echo $config['database']['connections'][$driver]['host'] . "\n";
echo $config['database']['connections']['mysql']['port'] . "\n";
Output
localhost
3306
The path through the brackets mirrors the structure: database → connections → mysql → host. Note that a variable ($driver) can be used at any level.
Reading Nested Values Safely
Reading a missing key at any level raises a warning. Use ?? for optional data:
<?php
$order = ['id' => 501, 'customer' => ['name' => 'Asha']];
echo $order['customer']['address']['city'] ?? 'City not provided';
echo "\n";
Output
City not provided
?? checks the whole path without warnings, even though address does not exist. See Access Array Items for a path-based helper.
Adding and Updating Nested Data
<?php
$students = [
['name' => 'Asha', 'marks' => ['math' => 90, 'science' => 85]],
['name' => 'Bala', 'marks' => ['math' => 72, 'science' => 68]],
];
$students[0]['marks']['english'] = 78; // add a field to one record
$students[1]['marks']['science'] += 5; // update a nested value
$students[] = ['name' => 'Charu', 'marks' => ['math' => 95]]; // add a new record
echo count($students) . " students\n";
echo "Bala science: {$students[1]['marks']['science']}\n";
echo "Asha subjects: " . implode(', ', array_keys($students[0]['marks'])) . "\n";
Output
3 students
Bala science: 73
Asha subjects: math, science, english
Assigning to a nested key creates any missing levels automatically.
Looping Through Nested Data
A nested loop visits every value:
<?php
$students = [
'Asha' => ['math' => 90, 'science' => 85, 'english' => 78],
'Bala' => ['math' => 72, 'science' => 68, 'english' => 88],
'Charu' => ['math' => 95, 'science' => 91, 'english' => 89],
];
foreach ($students as $name => $marks) {
$average = array_sum($marks) / count($marks);
printf("%-6s %6.2f\n", $name, $average);
}
echo "\nSubject averages:\n";
foreach (array_keys(reset($students)) as $subject) {
$average = array_sum(array_column($students, $subject)) / count($students);
printf("%-8s %6.2f\n", $subject, $average);
}
Output
Asha 84.33
Bala 76.00
Charu 91.67
Subject averages:
math 85.67
science 81.33
english 85.00
How It Works
- The outer loop goes through students; inner operations (
array_sum(),count()) work on each student's marks. - For subject averages,
reset($students)returns the first student's marks, andarray_keys()gives the subject names. array_column($students, $subject)extracts one subject's marks from every student — a column of the table.
array_column() is the key tool for reading "down" a column instead of "across" a row.
Counting Nested Elements
<?php
$students = [
['name' => 'Asha', 'marks' => [90, 85, 78]],
['name' => 'Bala', 'marks' => [72, 68, 88]],
['name' => 'Charu', 'marks' => [95, 91, 89]],
];
echo count($students) . "\n";
echo count($students, COUNT_RECURSIVE) . "\n";
Output
3
18
count()counts only the top level: 3 records.COUNT_RECURSIVEcounts every element at every level, including the inner arrays themselves: 3 records + 3 × (2 fields + 3 marks) = 18. This is rarely what you want for business logic; usually you count a specific level.
Rendering a Table of Records as HTML
<?php
$products = [
['sku' => 'PEN-01', 'name' => 'Gel Pen', 'price' => 20],
['sku' => 'NB-A5', 'name' => 'Notebook <A5>', 'price' => 60],
];
$headers = array_keys($products[0]);
?>
<table>
<tr>
<?php foreach ($headers as $header): ?>
<th><?= htmlspecialchars(ucfirst($header)) ?></th>
<?php endforeach; ?>
</tr>
<?php foreach ($products as $product): ?>
<tr>
<?php foreach ($product as $value): ?>
<td><?= htmlspecialchars((string) $value) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</table>
Output (HTML)
<table>
<tr>
<th>Sku</th>
<th>Name</th>
<th>Price</th>
</tr>
<tr>
<td>PEN-01</td>
<td>Gel Pen</td>
<td>20</td>
</tr>
<tr>
<td>NB-A5</td>
<td>Notebook <A5></td>
<td>60</td>
</tr>
</table>
The header row comes from the keys of the first record, and nested foreach loops produce rows and cells. Every value is escaped with htmlspecialchars().
Practical Example: Sales Report by Region and Month
<?php
$sales = [
'North' => ['Jan' => 120, 'Feb' => 135, 'Mar' => 150],
'South' => ['Jan' => 200, 'Feb' => 180, 'Mar' => 210],
'West' => ['Jan' => 90, 'Feb' => 110, 'Mar' => 95],
];
$months = array_keys($sales['North']);
$monthTotals = array_fill_keys($months, 0);
printf("%-6s %5s %5s %5s %6s\n", 'Region', ...$months, ...['Total']);
foreach ($sales as $region => $byMonth) {
foreach ($byMonth as $month => $amount) {
$monthTotals[$month] += $amount;
}
printf("%-6s %5d %5d %5d %6d\n", $region, ...array_values($byMonth), ...[array_sum($byMonth)]);
}
printf("%-6s %5d %5d %5d %6d\n", 'Total', ...array_values($monthTotals), ...[array_sum($monthTotals)]);
Output
Region Jan Feb Mar Total
North 120 135 150 405
South 200 180 210 590
West 90 110 95 295
Total 410 425 455 1290
Step-by-Step Explanation
- The outer keys are regions; the inner keys are months. Each value is a sales figure (in thousands of rupees, for example).
array_fill_keys($months, 0)creates a running total for each month.- The nested loop adds every figure to its month's total, while
array_sum($byMonth)gives each region's total (row total). - The spread operator (
...) passes array values as separateprintf()arguments. Spreading an array literal (...['Total']) is used because, in a function call, a normal positional argument cannot follow an unpacked array. - The final row prints the month totals and the grand total.
Row totals sum "across" the inner array; column totals require accumulating "down" through all outer elements.
When to Use Objects Instead
Deeply nested associative arrays become hard to manage: key typos are not detected, and nobody knows which keys a structure must contain. For important domain data (orders, invoices, users), classes with typed properties provide structure, validation, and editor support. Arrays remain ideal for simple tables, configuration, and data passing between layers. See the PHP OOP section.
Common Mistakes
Losing Track of the Level
Effect: $users['name'] instead of $users[0]['name'] — accessing a field on the list instead of on a record.
Fix: Name variables for what they hold ($users vs $user) and use var_dump() or print_r() to inspect structures.
Missing Keys in Nested Paths
Effect: Warnings, or errors when accessing offsets on null.
Fix: Use ?? for optional nested data.
Modifying the Loop Variable Instead of the Array
Effect: foreach ($users as $user) { $user['active'] = true; } changes nothing.
Fix: Write back with the key: $users[$i]['active'] = true;.
Inconsistent Record Shapes
Effect: Some records have keys that others lack, breaking loops and tables.
Fix: Normalize records when creating them, or use objects.
Best Practices
- Use lists of associative arrays for tabular data, and nested associative arrays for grouped settings.
- Access nested optional data with
??. - Use
array_column()to work with columns of record lists. - Keep nesting shallow; extract inner structures into functions or classes when they grow.
- Escape every value when rendering nested data as HTML.
Official reference: https://www.php.net/manual/en/language.types.array.php