🔁 Lesson 6: Loops
Computers are great at doing the same thing over and over — and that's exactly what loops are for. Instead of writing the same code 100 times, you write it once and let the loop handle the repetition.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Use
forloops for counted iteration - Use
whileanddo-whileloops for condition-based repetition - Iterate arrays with
foreach— PHP's most-used loop - Control loop flow with
breakandcontinue - Nest loops for multi-dimensional data processing
- Choose the right loop for each situation
Estimated Time: 45 minutes
Prerequisites: Lesson 5 (control flow, truthy/falsy)
📑 In This Lesson
Why Loops?
📖 Definition
Loop: A control structure that repeats a block of code as long as a specified condition remains true (or for a specified number of iterations).
Imagine you need to display a list of 50 products from a database. Without loops, you'd need 50 nearly identical blocks of code. With a loop, you write the display logic once:
<?php
// Without a loop (terrible!) 😱
echo "<li>" . $products[0] . "</li>";
echo "<li>" . $products[1] . "</li>";
echo "<li>" . $products[2] . "</li>";
// ... 47 more lines ...
// With a loop (much better!) ✅
foreach ($products as $product) {
echo "<li>$product</li>";
}
PHP provides four types of loops:
The for Loop
The for loop is best when you know how many times you want to iterate. It packs three pieces into one line: initialization, condition, and update.
Syntax
for (initialization; condition; update) {
// Code to repeat
}
Basic Examples
<?php
// Count from 1 to 5
for ($i = 1; $i <= 5; $i++) {
echo "$i ";
}
// Output: 1 2 3 4 5
// Count down from 10 to 1
for ($i = 10; $i >= 1; $i--) {
echo "$i ";
}
// Output: 10 9 8 7 6 5 4 3 2 1
// Count by twos
for ($i = 0; $i <= 20; $i += 2) {
echo "$i ";
}
// Output: 0 2 4 6 8 10 12 14 16 18 20
Practical: Building an HTML Table
<?php
echo "<table>\n";
echo " <tr><th>#</th><th>Square</th><th>Cube</th></tr>\n";
for ($n = 1; $n <= 10; $n++) {
$square = $n ** 2;
$cube = $n ** 3;
$class = ($n % 2 === 0) ? "even" : "odd";
echo " <tr class='$class'>";
echo "<td>$n</td><td>$square</td><td>$cube</td>";
echo "</tr>\n";
}
echo "</table>";
Looping Through Strings
<?php
$word = "PHP";
for ($i = 0; $i < strlen($word); $i++) {
echo "Character $i: $word[$i]\n";
}
// Output:
// Character 0: P
// Character 1: H
// Character 2: P
⚠️ Performance Tip
Don't call functions in the condition part — they run every iteration:
// BAD — strlen() called every iteration
for ($i = 0; $i < strlen($word); $i++) { ... }
// GOOD — calculate once
$length = strlen($word);
for ($i = 0; $i < $length; $i++) { ... }
Multiple Expressions
You can use commas to have multiple initializations or updates:
<?php
// Two counters moving in opposite directions
for ($left = 0, $right = 9; $left < $right; $left++, $right--) {
echo "L=$left, R=$right\n";
}
// Output:
// L=0, R=9
// L=1, R=8
// L=2, R=7
// L=3, R=6
// L=4, R=5
The while Loop
The while loop is best when you don't know in advance how many iterations you need. It checks the condition before each iteration.
Syntax
while (condition) {
// Code to repeat
// Must include something that eventually makes condition false!
}
Basic Examples
<?php
// Count down — we know when to stop, but while is still natural here
$countdown = 5;
while ($countdown > 0) {
echo "$countdown... ";
$countdown--;
}
echo "Liftoff! 🚀";
// Output: 5... 4... 3... 2... 1... Liftoff! 🚀
Practical: Reading User Input (Simulated)
<?php
// Simulating a menu system
$choices = ["view", "edit", "delete", "quit"]; // Simulated input
$index = 0;
while ($index < count($choices)) {
$choice = $choices[$index];
echo "Processing: $choice\n";
if ($choice === "quit") {
echo "Goodbye!\n";
break; // Exit early
}
$index++;
}
// Output:
// Processing: view
// Processing: edit
// Processing: delete
// Processing: quit
// Goodbye!
Practical: Halving Until Small Enough
<?php
// How many times can you halve 1000 before reaching less than 1?
$value = 1000;
$halvings = 0;
while ($value >= 1) {
$value /= 2;
$halvings++;
}
echo "It took $halvings halvings to get below 1.";
// Output: It took 10 halvings to get below 1.
⚠️ Infinite Loops!
If the condition never becomes false, the loop runs forever. This will crash your script (PHP will hit its max_execution_time and abort).
// DANGER — infinite loop!
$x = 1;
while ($x > 0) {
echo $x;
$x++; // x keeps growing — condition is always true!
}
// Always ensure your loop has a way to end:
// ✅ A counter that reaches its limit
// ✅ A condition that changes inside the loop
// ✅ A break statement that exits
The do-while Loop
The do-while loop is identical to while except it checks the condition after the body runs. This guarantees the body executes at least once.
Syntax
do {
// Code to repeat (runs at least once)
} while (condition);
while vs. do-while
<?php
// while — checks FIRST, body may never run
$x = 10;
while ($x < 5) {
echo "while: $x\n"; // Never runs! 10 is not < 5
$x++;
}
// do-while — runs FIRST, then checks
$x = 10;
do {
echo "do-while: $x\n"; // Runs once! Then condition fails
$x++;
} while ($x < 5);
// Output: do-while: 10
Practical: Input Validation Pattern
The do-while loop shines for validation — you need to get the input at least once before you can check it:
<?php
// Simulating user input validation
$attempts = ["", "abc", "-5", "42"]; // Simulated inputs
$index = 0;
do {
$input = $attempts[$index] ?? null;
$index++;
if ($input === null) {
echo "No more input.\n";
break;
}
$isValid = is_numeric($input) && (int)$input > 0;
if (!$isValid) {
echo "Invalid input: '$input'. Try again.\n";
}
} while (!$isValid);
if ($isValid) {
echo "Accepted: $input\n";
}
// Output:
// Invalid input: ''. Try again.
// Invalid input: 'abc'. Try again.
// Invalid input: '-5'. Try again.
// Accepted: 42
Practical: Generating Unique IDs
<?php
$existingIds = ["usr_001", "usr_002", "usr_003"];
do {
$newId = "usr_" . str_pad(rand(1, 999), 3, "0", STR_PAD_LEFT);
} while (in_array($newId, $existingIds));
echo "Generated unique ID: $newId";
✅ When to Use do-while
Use do-while when the action must happen before you can evaluate the condition. Common scenarios: input validation, generating values until one meets criteria, menu loops, and retry logic.
The foreach Loop
The foreach loop is PHP's most-used loop. It's purpose-built for iterating over arrays and objects — no counters, no index management, no off-by-one errors.
Syntax: Values Only
foreach ($array as $value) {
// $value is the current element
}
Syntax: Keys and Values
foreach ($array as $key => $value) {
// $key is the current key/index
// $value is the current element
}
Indexed Arrays
<?php
$fruits = ["Apple", "Banana", "Cherry", "Date"];
// Values only
foreach ($fruits as $fruit) {
echo "🍎 $fruit\n";
}
// Output:
// 🍎 Apple
// 🍎 Banana
// 🍎 Cherry
// 🍎 Date
// With index
foreach ($fruits as $index => $fruit) {
$num = $index + 1;
echo "$num. $fruit\n";
}
// Output:
// 1. Apple
// 2. Banana
// 3. Cherry
// 4. Date
Associative Arrays
<?php
$user = [
"name" => "Alice",
"email" => "alice@example.com",
"role" => "admin",
"age" => 28,
];
foreach ($user as $key => $value) {
echo ucfirst($key) . ": $value\n";
}
// Output:
// Name: Alice
// Email: alice@example.com
// Role: admin
// Age: 28
Practical: Rendering an HTML List
<?php
$tasks = [
["title" => "Buy groceries", "done" => true],
["title" => "Write PHP lesson", "done" => false],
["title" => "Walk the dog", "done" => true],
["title" => "Fix login bug", "done" => false],
];
echo "<ul class='task-list'>\n";
foreach ($tasks as $task) {
$icon = $task["done"] ? "✅" : "⬜";
$class = $task["done"] ? "completed" : "pending";
echo " <li class='$class'>$icon {$task['title']}</li>\n";
}
echo "</ul>";
// Output:
//
// - ✅ Buy groceries
// - ⬜ Write PHP lesson
// - ✅ Walk the dog
// - ⬜ Fix login bug
//
Modifying Values with Reference (&)
By default, foreach gives you a copy of each value. To modify the original array, use a reference (&):
<?php
$prices = [10.50, 20.00, 15.75, 8.99];
// This does NOT modify the original array (copy)
foreach ($prices as $price) {
$price *= 1.10; // 10% increase — only modifies the copy
}
print_r($prices); // Still the original values!
// This DOES modify the original array (reference)
foreach ($prices as &$price) {
$price *= 1.10; // 10% increase — modifies the original
}
unset($price); // ⚠️ Always unset the reference after the loop!
print_r($prices);
// Output: [11.55, 22.0, 17.325, 9.889]
⚠️ Always unset() After Reference Loops
After a foreach with &$value, the variable $value still points to the last array element. If you reuse that variable later, you'll accidentally modify the array:
$colors = ["red", "green", "blue"];
foreach ($colors as &$color) {
$color = strtoupper($color);
}
// $color still references $colors[2]!
$color = "yellow";
print_r($colors); // ["RED", "GREEN", "yellow"] ← BUG!
// Fix: unset($color) right after the loop
foreach ($colors as &$color) {
$color = strtoupper($color);
}
unset($color); // ✅ Breaks the reference
foreach with list() for Destructuring
<?php
$coordinates = [[1, 2], [3, 4], [5, 6]];
foreach ($coordinates as [$x, $y]) {
echo "Point: ($x, $y)\n";
}
// Output:
// Point: (1, 2)
// Point: (3, 4)
// Point: (5, 6)
break and continue
These two keywords give you fine-grained control over loop execution.
| Keyword | Effect | Analogy |
|---|---|---|
break |
Exits the loop entirely | Walking out of the building |
continue |
Skips the rest of the current iteration | Skipping one step on the staircase |
break — Exit Early
<?php
// Find the first negative number
$numbers = [4, 7, 2, -3, 8, -1, 5];
foreach ($numbers as $num) {
if ($num < 0) {
echo "Found negative number: $num\n";
break; // Stop searching — we found what we need
}
echo "Checked: $num (positive)\n";
}
// Output:
// Checked: 4 (positive)
// Checked: 7 (positive)
// Checked: 2 (positive)
// Found negative number: -3
continue — Skip an Iteration
<?php
// Print only even numbers
for ($i = 1; $i <= 10; $i++) {
if ($i % 2 !== 0) {
continue; // Skip odd numbers
}
echo "$i ";
}
// Output: 2 4 6 8 10
// Skip inactive users
$users = [
["name" => "Alice", "active" => true],
["name" => "Bob", "active" => false],
["name" => "Carol", "active" => true],
["name" => "Dave", "active" => false],
];
foreach ($users as $user) {
if (!$user["active"]) {
continue; // Skip inactive users
}
echo "Sending email to {$user['name']}...\n";
}
// Output:
// Sending email to Alice...
// Sending email to Carol...
break with Nested Loops
By default, break only exits the innermost loop. You can pass a number to break out of multiple levels:
<?php
// break 1 — exits inner loop only (default)
// break 2 — exits inner AND outer loop
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
if ($i === 1 && $j === 1) {
break 2; // Exit BOTH loops
}
echo "[$i,$j] ";
}
}
echo "\nDone!";
// Output: [0,0] [0,1] [0,2] [1,0]
// Done!
✅ When to Use break vs. continue
break: Use when you've found what you're looking for and don't need to keep looping (search results, error conditions, early exit).
continue: Use when certain items should be skipped but you still need to process the rest (filtering, skipping invalid data).
Nested Loops
A loop inside a loop. The inner loop runs completely for each iteration of the outer loop. This is essential for working with multi-dimensional data.
How Nested Loops Work
<?php
// Outer loop runs 3 times
// For each outer iteration, inner loop runs 4 times
// Total iterations: 3 × 4 = 12
for ($row = 1; $row <= 3; $row++) {
for ($col = 1; $col <= 4; $col++) {
echo "($row,$col) ";
}
echo "\n";
}
// Output:
// (1,1) (1,2) (1,3) (1,4)
// (2,1) (2,2) (2,3) (2,4)
// (3,1) (3,2) (3,3) (3,4)
Practical: Multiplication Table
<?php
echo "<table border='1'>\n";
// Header row
echo "<tr><th>×</th>";
for ($col = 1; $col <= 10; $col++) {
echo "<th>$col</th>";
}
echo "</tr>\n";
// Data rows
for ($row = 1; $row <= 10; $row++) {
echo "<tr><th>$row</th>";
for ($col = 1; $col <= 10; $col++) {
$product = $row * $col;
echo "<td>$product</td>";
}
echo "</tr>\n";
}
echo "</table>";
Practical: Iterating Nested Arrays
<?php
$students = [
["name" => "Alice", "grades" => [92, 88, 95]],
["name" => "Bob", "grades" => [78, 85, 72]],
["name" => "Carol", "grades" => [95, 97, 100]],
];
foreach ($students as $student) {
$total = 0;
$count = count($student["grades"]);
foreach ($student["grades"] as $grade) {
$total += $grade;
}
$average = $total / $count;
echo "{$student['name']}: avg = " . number_format($average, 1) . "\n";
}
// Output:
// Alice: avg = 91.7
// Bob: avg = 78.3
// Carol: avg = 97.3
Practical: Building a Star Pattern
<?php
$rows = 5;
// Right triangle
for ($i = 1; $i <= $rows; $i++) {
for ($j = 1; $j <= $i; $j++) {
echo "⭐";
}
echo "\n";
}
// Output:
// ⭐
// ⭐⭐
// ⭐⭐⭐
// ⭐⭐⭐⭐
// ⭐⭐⭐⭐⭐
⚠️ Watch the Performance
Nested loops multiply their iterations. A loop of 1,000 inside a loop of 1,000 = 1,000,000 iterations. Three levels deep with 100 each = 1,000,000. Always be mindful of nested loop depth with large datasets.
Choosing the Right Loop
| Loop | Best For | Example |
|---|---|---|
foreach |
Iterating arrays/objects | Display all products, process form data |
for |
Known number of iterations | Generate 10 rows, count from 1 to 100 |
while |
Unknown iterations, check first | Read file lines, wait for condition |
do-while |
Must run at least once | Input validation, menu display, retry logic |
Common Patterns
Pattern: Accumulator
<?php
// Sum all values in an array
$prices = [12.99, 24.50, 7.25, 18.00];
$total = 0;
foreach ($prices as $price) {
$total += $price;
}
echo "Total: $" . number_format($total, 2);
// Output: Total: $62.74
Pattern: Search
<?php
// Find the first matching item
$users = ["alice", "bob", "carol", "dave"];
$search = "carol";
$found = false;
foreach ($users as $index => $user) {
if ($user === $search) {
echo "Found '$search' at index $index";
$found = true;
break;
}
}
if (!$found) {
echo "'$search' not found.";
}
// Output: Found 'carol' at index 2
Pattern: Filter
<?php
// Build a new array with only items that pass a test
$numbers = [3, 7, 12, 5, 18, 2, 9, 15];
$bigNumbers = [];
foreach ($numbers as $num) {
if ($num >= 10) {
$bigNumbers[] = $num;
}
}
print_r($bigNumbers);
// Output: [12, 18, 15]
Pattern: Transform
<?php
// Create a new array by transforming each element
$names = ["alice", "bob", "carol"];
$formatted = [];
foreach ($names as $name) {
$formatted[] = ucfirst($name);
}
print_r($formatted);
// Output: ["Alice", "Bob", "Carol"]
💡 Preview: In Lesson 9, you'll learnarray_filter(),array_map(), andarray_reduce()— built-in functions that replace many of these manual loop patterns with one-liners.
Hands-On Exercises
🏋️ Exercise 1: FizzBuzz
Objective: The classic programming challenge! Print numbers 1 to 30, but:
- For multiples of 3, print "Fizz" instead of the number
- For multiples of 5, print "Buzz" instead of the number
- For multiples of both 3 and 5, print "FizzBuzz"
💡 Hint
Check for divisibility by both 3 AND 5 first (otherwise the individual checks will match first). Use the modulo operator (%) — a number is divisible by N if $num % N === 0.
✅ Solution
<?php
for ($i = 1; $i <= 30; $i++) {
if ($i % 3 === 0 && $i % 5 === 0) {
echo "FizzBuzz";
} elseif ($i % 3 === 0) {
echo "Fizz";
} elseif ($i % 5 === 0) {
echo "Buzz";
} else {
echo $i;
}
echo "\n";
}
// 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz,
// 11, Fizz, 13, 14, FizzBuzz, 16, ...
🏋️ Exercise 2: Shopping Cart Total
Objective: Use foreach to calculate the total cost of a shopping cart, applying discounts and formatting the output.
Starter Code:
<?php
$cart = [
["name" => "Widget", "price" => 9.99, "qty" => 3],
["name" => "Gadget", "price" => 24.99, "qty" => 1],
["name" => "Doohickey", "price" => 4.50, "qty" => 5],
["name" => "Thingamajig","price" => 15.00, "qty" => 2],
];
$discountThreshold = 50; // Orders over $50 get 10% off
// TODO: Loop through cart, calculate line totals and grand total
// TODO: Apply discount if over threshold
// TODO: Display formatted receipt
💡 Hint
Use an accumulator variable for the grand total. For each item, calculate $price * $qty. After the loop, check if the total exceeds the threshold and apply a 10% discount.
✅ Solution
<?php
$cart = [
["name" => "Widget", "price" => 9.99, "qty" => 3],
["name" => "Gadget", "price" => 24.99, "qty" => 1],
["name" => "Doohickey", "price" => 4.50, "qty" => 5],
["name" => "Thingamajig","price" => 15.00, "qty" => 2],
];
$discountThreshold = 50;
$subtotal = 0;
echo "===== SHOPPING CART =====\n";
echo str_pad("Item", 15) . str_pad("Qty", 5) . str_pad("Price", 10) . "Line Total\n";
echo str_repeat("-", 45) . "\n";
foreach ($cart as $item) {
$lineTotal = $item["price"] * $item["qty"];
$subtotal += $lineTotal;
echo str_pad($item["name"], 15);
echo str_pad($item["qty"], 5);
echo str_pad("$" . number_format($item["price"], 2), 10);
echo "$" . number_format($lineTotal, 2) . "\n";
}
echo str_repeat("-", 45) . "\n";
echo "Subtotal: $" . number_format($subtotal, 2) . "\n";
if ($subtotal > $discountThreshold) {
$discount = $subtotal * 0.10;
$total = $subtotal - $discount;
echo "Discount (10%): -$" . number_format($discount, 2) . "\n";
echo "TOTAL: $" . number_format($total, 2) . "\n";
} else {
echo "TOTAL: $" . number_format($subtotal, 2) . "\n";
echo "(Spend over $$discountThreshold for 10% off!)\n";
}
🏋️ Exercise 3: Password Validator
Objective: Use a for loop (and optionally while) to check password strength by iterating through each character.
Rules:
- At least 8 characters long
- Contains at least one uppercase letter
- Contains at least one lowercase letter
- Contains at least one digit
- Contains at least one special character (!@#$%^&*)
💡 Hint
Use ctype_upper(), ctype_lower(), ctype_digit() to check individual characters. Use strpos() or str_contains() with a string of special characters.
✅ Solution
<?php
$password = "MyP@ss1234";
$hasUpper = false;
$hasLower = false;
$hasDigit = false;
$hasSpecial = false;
$specials = "!@#$%^&*";
$errors = [];
// Check length first
if (strlen($password) < 8) {
$errors[] = "Must be at least 8 characters";
}
// Check each character
for ($i = 0; $i < strlen($password); $i++) {
$char = $password[$i];
if (ctype_upper($char)) {
$hasUpper = true;
} elseif (ctype_lower($char)) {
$hasLower = true;
} elseif (ctype_digit($char)) {
$hasDigit = true;
} elseif (str_contains($specials, $char)) {
$hasSpecial = true;
}
}
if (!$hasUpper) $errors[] = "Missing uppercase letter";
if (!$hasLower) $errors[] = "Missing lowercase letter";
if (!$hasDigit) $errors[] = "Missing digit";
if (!$hasSpecial) $errors[] = "Missing special character";
if (empty($errors)) {
echo "✅ Password is strong!\n";
} else {
echo "❌ Password is weak:\n";
foreach ($errors as $error) {
echo " - $error\n";
}
}
// Output: ✅ Password is strong!
🎯 Quick Quiz
Question 1: Which loop is best for iterating over an associative array?
Question 2: What does continue do inside a loop?
Question 3: What is the key difference between while and do-while?
Question 4: Why should you call unset($value) after a foreach loop that uses &$value?
Question 5: What does break 2 do inside nested loops?
Summary
🎉 Key Takeaways
- for — Best for counted iterations; packs init, condition, and update in one line
- while — Best when you don't know the iteration count; checks condition before each pass
- do-while — Like while, but guarantees at least one execution; great for validation and retries
- foreach — PHP's go-to for arrays; use
$key => $valuefor associative arrays - break — Exits the loop entirely; use
break Nto exit N levels of nesting - continue — Skips the rest of the current iteration and moves to the next
- References — Use
&$valuein foreach to modify the original array; alwaysunset()after - Performance — Watch nested loop depth; avoid function calls in loop conditions
📚 Additional Resources
🚀 What's Next?
Now that you can repeat code and iterate data, it's time to organize your code. In Lesson 7: Functions, you'll learn to bundle reusable logic into functions — with parameters, return values, type hints, and more.
🎉 Congratulations!
You've mastered PHP's loop toolkit! From for to foreach, you can now iterate, search, filter, and transform data like a pro. Module 2 is almost complete!