Nested Loops
What Is a Nested Loop? In simple terms: A nested loop is a loop inside another loop. For each iteration of the outer loop, the inner loop runs completely from start to finish. Technically: Any loop (
What Is a Nested Loop?
In simple terms: A nested loop is a loop inside another loop. For each iteration of the outer loop, the inner loop runs completely from start to finish.
Technically: Any loop (for, foreach, while, do...while) can contain another loop in its body. If the outer loop runs m times and the inner loop runs n times per outer iteration, the inner body executes m × n times.
Why Nested Loops Are Needed
Many kinds of data have two (or more) dimensions:
- Rows and columns of a table or grid
- Orders, each with several items
- Categories, each with several products
- Every combination of sizes and colors
A nested loop visits each item in such structures.
Syntax
foreach ($outerItems as $outer) {
// runs once per outer item
foreach ($innerItems as $inner) {
// runs once per combination of $outer and $inner
}
}
Example: Visualizing the Order of Execution
<?php
for ($row = 1; $row <= 3; $row++) {
for ($col = 1; $col <= 2; $col++) {
echo "row $row, col $col\n";
}
}
Output
row 1, col 1
row 1, col 2
row 2, col 1
row 2, col 2
row 3, col 1
row 3, col 2
How It Works
- The outer loop starts with
$row = 1. - The inner loop runs completely:
$col = 1, then$col = 2. - Only then does the outer loop move to
$row = 2, and the inner loop starts again from$col = 1. - Total iterations of the inner body: 3 × 2 = 6.
Think of reading a book: for each line (outer loop), you read every word on that line (inner loop) before moving to the next line.
Practical Example: Multiplication Table
<?php
$size = 4;
for ($i = 1; $i <= $size; $i++) {
for ($j = 1; $j <= $size; $j++) {
printf("%4d", $i * $j);
}
echo "\n";
}
Output
1 2 3 4
2 4 6 8
3 6 9 12
4 8 12 16
- The outer loop selects the row number
$i. - The inner loop prints one cell per column
$j, each value right-aligned in 4 characters with%4d. - The
echo "\n"is placed after the inner loop but inside the outer loop, so a line break is printed at the end of each row.
Where you place code — inside the inner loop, between the loops, or after them — determines how often it runs.
Practical Example: Orders and Their Items
Real data is often a list of records where each record contains its own list:
<?php
$orders = [
[
'id' => 501,
'items' => [
['name' => 'Notebook', 'qty' => 3, 'price' => 60],
['name' => 'Pen', 'qty' => 10, 'price' => 15],
],
],
[
'id' => 502,
'items' => [
['name' => 'Backpack', 'qty' => 1, 'price' => 1299],
],
],
];
foreach ($orders as $order) {
echo "Order #{$order['id']}\n";
$orderTotal = 0;
foreach ($order['items'] as $item) {
$lineTotal = $item['qty'] * $item['price'];
$orderTotal += $lineTotal;
printf(" %-10s %2d × ₹%-5d = ₹%d\n", $item['name'], $item['qty'], $item['price'], $lineTotal);
}
echo " Total: ₹$orderTotal\n";
}
Output
Order #501
Notebook 3 × ₹60 = ₹180
Pen 10 × ₹15 = ₹150
Total: ₹330
Order #502
Backpack 1 × ₹1299 = ₹1299
Total: ₹1299
How It Works
- The outer loop visits each order and resets
$orderTotalto0for that order. - The inner loop visits each item of the current order (
$order['items']) and adds its line total. - After the inner loop finishes, the total for that order is printed.
Placing $orderTotal = 0; inside the outer loop (but before the inner loop) is crucial. If it were before the outer loop, totals would accumulate across orders.
Practical Example: All Combinations
Nested loops generate every combination of two lists — for example, product variants:
<?php
$sizes = ['S', 'M', 'L'];
$colors = ['Red', 'Blue'];
$skus = [];
foreach ($sizes as $size) {
foreach ($colors as $color) {
$skus[] = 'TSHIRT-' . $size . '-' . strtoupper($color);
}
}
echo implode("\n", $skus) . "\n";
echo count($skus) . " variants\n";
Output
TSHIRT-S-RED
TSHIRT-S-BLUE
TSHIRT-M-RED
TSHIRT-M-BLUE
TSHIRT-L-RED
TSHIRT-L-BLUE
6 variants
3 sizes × 2 colors = 6 variants. Adding a third dimension (for example, sleeve length) would require a third nested loop.
Practical Example: Drawing a Pattern
Pattern exercises demonstrate how the inner loop can depend on the outer loop's counter:
<?php
$height = 4;
for ($row = 1; $row <= $height; $row++) {
for ($space = 1; $space <= $height - $row; $space++) {
echo ' ';
}
for ($star = 1; $star <= 2 * $row - 1; $star++) {
echo '*';
}
echo "\n";
}
Output
*
***
*****
*******
How It Works
| Row | Spaces (height - row) | Stars (2 × row - 1) |
|---|---|---|
| 1 | 3 | 1 |
| 2 | 2 | 3 |
| 3 | 1 | 5 |
| 4 | 0 | 7 |
The number of inner iterations is calculated from the outer counter. In production code, str_repeat(' ', $height - $row) . str_repeat('*', 2 * $row - 1) produces the same line without inner loops.
Performance: The Cost of Nesting
The inner body of nested loops runs outer count × inner count times:
| Outer Items | Inner Items | Inner Iterations |
|---|---|---|
| 10 | 10 | 100 |
| 1,000 | 1,000 | 1,000,000 |
| 10,000 | 10,000 | 100,000,000 |
Nested loops over small lists are fine. Over large lists — especially when the inner loop only searches for a matching item — they become a serious performance problem.
Slow: Searching in an Inner Loop
<?php
$customers = [
['id' => 1, 'name' => 'Asha'],
['id' => 2, 'name' => 'Bala'],
];
$orders = [
['id' => 101, 'customer_id' => 2, 'total' => 500],
['id' => 102, 'customer_id' => 1, 'total' => 1200],
['id' => 103, 'customer_id' => 2, 'total' => 300],
];
foreach ($orders as $order) {
foreach ($customers as $customer) {
if ($customer['id'] === $order['customer_id']) {
echo "Order {$order['id']}: {$customer['name']} (₹{$order['total']})\n";
break;
}
}
}
Fast: Build a Lookup Array Once
<?php
$customerNames = array_column($customers, 'name', 'id'); // [1 => 'Asha', 2 => 'Bala']
foreach ($orders as $order) {
$name = $customerNames[$order['customer_id']] ?? 'Unknown customer';
echo "Order {$order['id']}: $name (₹{$order['total']})\n";
}
Output (both versions)
Order 101: Bala (₹500)
Order 102: Asha (₹1200)
Order 103: Bala (₹300)
Why the Second Version Scales
array_column($customers, 'name', 'id')creates an array where each customer's id is the key and the name is the value.- Looking up an array element by key is a constant-time operation, regardless of the array's size.
- The work becomes orders + customers instead of orders × customers. With 10,000 orders and 10,000 customers, that is about 20,000 operations instead of up to 100 million.
In real applications, this kind of matching is usually done by the database with a SQL JOIN (see MySQL Joins with PHP).
Controlling Nested Loops
break and continue affect the innermost loop by default. A numeric argument targets outer loops:
| Statement | Effect |
|---|---|
break | Exit the inner loop |
break 2 | Exit the inner and the outer loop |
continue | Skip to the next iteration of the inner loop |
continue 2 | Skip to the next iteration of the outer loop |
<?php
$seatMap = [
'A' => [1, 1, 1, 1], // 1 = taken, 0 = free
'B' => [1, 1, 0, 1],
'C' => [0, 0, 0, 0],
];
$found = null;
foreach ($seatMap as $row => $seats) {
foreach ($seats as $index => $taken) {
if ($taken === 0) {
$found = $row . ($index + 1);
break 2;
}
}
}
echo "First free seat: " . ($found ?? 'none') . "\n";
Output
First free seat: B3
As soon as a free seat is found, break 2 stops both loops, so no further seats are checked. Details are in Break Statement and Continue Statement.
Common Mistakes
Reusing the Same Counter Variable
for ($i = 0; $i < 3; $i++) {
for ($i = 0; $i < 2; $i++) { ... } // overwrites the outer $i
}
Effect: The inner loop resets the outer counter, causing an infinite loop or skipped iterations.
Fix: Use different names ($row/$col, $i/$j, $order/$item).
Resetting Accumulators in the Wrong Place
Effect: Totals accumulate across outer items, or reset too often.
Fix: Initialize per-outer-item values inside the outer loop, before the inner loop.
Searching With Nested Loops Over Large Data
Effect: Very slow code.
Fix: Build lookup arrays with array_column() or keys, or let the database join the data.
Deep Nesting
Effect: Code with three or more nested loops is hard to read.
Fix: Move inner loops into well-named functions, such as calculateOrderTotal($order).
Best Practices
- Use clear, distinct variable names for each loop level.
- Place each statement at the level where it belongs: per row, per cell, or once overall.
- Replace "search in inner loop" patterns with lookup arrays.
- Use
break 2/continue 2to control outer loops when needed. - Extract inner loops into functions when the logic grows.
Official reference: https://www.php.net/manual/en/control-structures.for.php