Skip to main content

🔁 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 for loops for counted iteration
  • Use while and do-while loops for condition-based repetition
  • Iterate arrays with foreach — PHP's most-used loop
  • Control loop flow with break and continue
  • 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:

flowchart LR A["PHP Loops"] --> B["for"] A --> C["while"] A --> D["do-while"] A --> E["foreach"] B --- B1["Know iteration count"] C --- C1["Condition checked first"] D --- D1["Runs at least once"] E --- E1["Iterate arrays/objects"]

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
}
flowchart TD A["Initialize: $i = 0"] --> B{"Condition: $i < 5?"} B -->|Yes| C["Execute loop body"] C --> D["Update: $i++"] D --> B B -->|No| E["Exit loop"]

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!
}
flowchart TD A["Start"] --> B{"Condition true?"} B -->|Yes| C["Execute loop body"] C --> B B -->|No| D["Exit loop"]

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);
flowchart TD A["Start"] --> B["Execute loop body"] B --> C{"Condition true?"} C -->|Yes| B C -->|No| D["Exit loop"]

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

flowchart TD A["Need to repeat?"] --> B{"Iterating an array?"} B -->|Yes| C["foreach ✅"] B -->|No| D{"Know how many times?"} D -->|Yes| E["for"] D -->|No| F{"Must run at least once?"} F -->|Yes| G["do-while"] F -->|No| H["while"]
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 learn array_filter(), array_map(), and array_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:

  1. For multiples of 3, print "Fizz" instead of the number
  2. For multiples of 5, print "Buzz" instead of the number
  3. 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:

  1. At least 8 characters long
  2. Contains at least one uppercase letter
  3. Contains at least one lowercase letter
  4. Contains at least one digit
  5. 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 => $value for associative arrays
  • break — Exits the loop entirely; use break N to exit N levels of nesting
  • continue — Skips the rest of the current iteration and moves to the next
  • References — Use &$value in foreach to modify the original array; always unset() 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!