Loops #
Loops are one of the most fundamental constructs in programming — running a block of code repeatedly without having to write it over and over. PHP provides four kinds of loops: while, do...while, for, and foreach, each with a different use context. What separates good loop code from bad isn’t just the syntax — it’s choosing the most expressive loop type for a given situation, avoiding the reference trap in foreach, knowing when break and continue are appropriate, and recognizing when array functions like array_map or array_filter are cleaner than explicit loops.
The PHP Loop Map #
Before diving into the details of each, here’s the big picture of when to use each loop type:
flowchart TD
A{What is being iterated?} --> B{Array or Iterable?}
B -- Yes --> C[foreach\nMost idiomatic for arrays]
B -- No --> D{Iteration count\nknown in advance?}
D -- Yes --> E[for\nIf you need an explicit numeric index]
D -- No --> F{Must run\nat least once?}
F -- Yes --> G[do-while\nInput validation, retry loops]
F -- No --> H[while\nCondition checked at the start]
style C fill:#dcfce7
style E fill:#dbeafe
style G fill:#fef9c3
style H fill:#fef9c3while — Condition Checked at the Start
#
while is the simplest loop: as long as the condition evaluates to true, the code block runs. The condition is checked before each iteration — if the condition is already false from the start, the block never runs at all.
<?php
// Basic pattern
$i = 0;
while ($i < 5) {
echo $i . " ";
$i++;
}
// Output: 0 1 2 3 4
while is most appropriate when the number of iterations isn’t known in advance and depends on a condition that can change at any time while the loop is running.
Reading a File Line by Line #
<?php
// while is very idiomatic for reading streams — the line count is unknown
$handle = fopen('data.csv', 'r');
if ($handle === false) {
throw new \RuntimeException("Failed to open file");
}
$lineCount = 0;
while (($line = fgets($handle)) !== false) {
$lineCount++;
$data = str_getcsv(trim($line));
// process each line...
echo "Line $lineCount: " . implode(', ', $data) . "\n";
}
fclose($handle);
echo "Total: $lineCount lines processed";
Polling and Retry #
<?php
// Retry with exponential backoff — the number of attempts isn't certain
$maxAttempts = 5;
$attempts = 0;
$success = false;
while ($attempts < $maxAttempts && !$success) {
$attempts++;
try {
$response = sendRequest('https://api.example.com/data');
$success = true;
echo "Succeeded on attempt $attempts";
} catch (\Exception $e) {
if ($attempts < $maxAttempts) {
$delay = 2 ** $attempts; // 2, 4, 8, 16 seconds
echo "Failed, retrying in {$delay}s...\n";
sleep($delay);
}
}
}
if (!$success) {
throw new \RuntimeException("Failed after $maxAttempts attempts");
}
Trap: Infinite Loops #
<?php
// ANTI-PATTERN: forgetting to increment/decrement causes an infinite loop
$i = 0;
while ($i < 10) {
echo $i;
// forgot $i++; — the loop never stops!
}
// ANTI-PATTERN: a condition that never becomes false
$data = fetchData();
while ($data) {
processData($data);
// forgot to assign the next $data — infinite loop!
}
// CORRECT: make sure the condition definitely moves toward false
$data = fetchData();
while ($data !== null) {
processData($data);
$data = fetchNextData(); // the condition definitely moves toward null
}
do...while — Executes at Least Once
#
do...while reverses the order: the code block runs first, then the condition is checked at the end. This guarantees the block always executes at least once, regardless of the initial condition value.
<?php
// Basic syntax
$i = 0;
do {
echo $i . " ";
$i++;
} while ($i < 5);
// Output: 0 1 2 3 4
// Even if the condition is already false from the start, it still runs once
$i = 100;
do {
echo "This still executes even though $i > 5\n";
$i++;
} while ($i < 5);
// Output: "This still executes even though 100 > 5"
do...while is most appropriate for input validation and operations that need one execution before the condition can be checked:
<?php
// Interactive input validation — ask first, check after getting it
do {
echo "Enter a number between 1-10: ";
$input = (int) trim(fgets(STDIN));
if ($input < 1 || $input > 10) {
echo "Invalid input, try again.\n";
}
} while ($input < 1 || $input > 10);
echo "Valid input: $input\n";
// Process a page with pagination — fetch the first page first,
// then check whether there's a next page
$page = 1;
$all = [];
do {
$result = fetchData($page, $perPage = 100);
$all = array_merge($all, $result['data']);
$page++;
} while ($page <= $result['total_pages']);
echo "Total data: " . count($all);
for — Structured Iteration with an Index
#
for is the most structured loop — initialization, condition, and step expression are all written on one line. Most appropriate when the iteration count is known in advance and you need access to a numeric index.
<?php
// Basic syntax: for (initialization; condition; step)
for ($i = 0; $i < 5; $i++) {
echo $i . " ";
}
// Output: 0 1 2 3 4
// All three parts are entirely optional — any can be left empty
// (but at least have an exit condition so it doesn't loop forever)
$i = 0;
for (; $i < 5; ) { // initialization and step moved outside/inside
echo $i . " ";
$i++;
}
// Backward loop
for ($i = 10; $i >= 0; $i--) {
echo $i . " ";
}
// Output: 10 9 8 7 6 5 4 3 2 1 0
// Step greater than 1
for ($i = 0; $i <= 100; $i += 10) {
echo $i . " ";
}
// Output: 0 10 20 30 40 50 60 70 80 90 100
for with Arrays — When You Need an Index
#
<?php
$items = ['apple', 'mango', 'orange', 'grape', 'watermelon'];
// Access elements by index
for ($i = 0; $i < count($items); $i++) {
echo "$i: {$items[$i]}\n";
}
// ANTI-PATTERN: count() called again every iteration — wasteful
for ($i = 0; $i < count($items); $i++) { /* ... */ }
// CORRECT: store the array length outside the loop
$length = count($items);
for ($i = 0; $i < $length; $i++) {
echo "$i: {$items[$i]}\n";
}
// for is useful when you need two indices at once
for ($i = 0, $j = count($items) - 1; $i < $j; $i++, $j--) {
// swap elements from the front and back
[$items[$i], $items[$j]] = [$items[$j], $items[$i]];
}
// Result: the array order is reversed
print_r($items); // ['watermelon', 'grape', 'orange', 'mango', 'apple']
Nested Loops with for
#
<?php
// Multiplication table — natural nesting for two dimensions
for ($row = 1; $row <= 5; $row++) {
for ($col = 1; $col <= 5; $col++) {
printf("%4d", $row * $col);
}
echo "\n";
}
/*
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
5 10 15 20 25
*/
// Generating a 2D matrix
$matrix = [];
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
$matrix[$i][$j] = $i * 3 + $j + 1;
}
}
// [[1,2,3], [4,5,6], [7,8,9]]
foreach — Iterating Arrays and Iterables
#
foreach is the most idiomatic way to iterate over arrays in PHP. No manual index management needed — PHP handles all of that internally.
<?php
// Indexed array — values only
$fruits = ['apple', 'mango', 'orange'];
foreach ($fruits as $item) {
echo $item . "\n";
}
// Indexed array — index and value
foreach ($fruits as $index => $item) {
echo "$index: $item\n";
}
// 0: apple 1: mango 2: orange
// Associative array — key and value
$prices = ['apple' => 5000, 'mango' => 8000, 'orange' => 6000];
foreach ($prices as $name => $value) {
echo "$name: Rp " . number_format($value) . "\n";
}
// Multidimensional array — destructure directly
$users = [
['id' => 1, 'name' => 'Budi', 'role' => 'admin'],
['id' => 2, 'name' => 'Siti', 'role' => 'editor'],
['id' => 3, 'name' => 'Dani', 'role' => 'viewer'],
];
foreach ($users as ['id' => $id, 'name' => $name, 'role' => $role]) {
echo "[$id] $name — $role\n";
}
foreach Works on a Copy
#
foreach works on a copy of the array by default, not the original array. Modifying $item inside the loop doesn’t change the array:
<?php
$numbers = [1, 2, 3, 4, 5];
// ANTI-PATTERN: modifying $item doesn't change the original array
foreach ($numbers as $item) {
$item *= 2; // modifies the copy, not the original element
}
print_r($numbers); // [1, 2, 3, 4, 5] — unchanged!
// To modify array elements, there are three ways:
// Way 1: use a reference (&)
foreach ($numbers as &$item) {
$item *= 2;
}
unset($item); // REQUIRED! release the reference after the loop
print_r($numbers); // [2, 4, 6, 8, 10]
// Way 2: modify via index (more explicit)
foreach ($numbers as $i => $item) {
$numbers[$i] = $item * 2;
}
// Way 3: array_map (most functional, doesn't modify the original)
$doubled = array_map(fn($n) => $n * 2, $numbers);
The Reference Trap in foreach
#
This is one of the subtlest bugs in PHP and a source of confusion even for experienced developers:
<?php
$data = [1, 2, 3, 4, 5];
foreach ($data as &$value) {
$value *= 2;
}
// After the loop: $value still references the LAST element of the array ($data[4])
// Now $value is an alias for $data[4]
// The next foreach loop or any assignment to $value will change $data[4]!
foreach ($data as $value) { // $value is now an alias for $data[4]
// each iteration assigns the current element's value to $value
// which ALSO CHANGES $data[4] because $value still references it!
}
print_r($data);
// Result: [2, 4, 6, 8, 8] — the last element becomes 8 (not 10!)
// This is a very hard-to-find bug
// SOLUTION: always unset() the reference after a foreach with &
foreach ($data as &$value) {
$value *= 2;
}
unset($value); // break the reference — required!
// Now it's safe for the next loop
foreach ($data as $value) {
echo $value . " "; // 2 4 6 8 10 — correct
}
Always callunset($var)after aforeachthat uses a reference (&$var). Without it, the loop variable still points at the array’s last element as an alias. Any later loop or assignment using the same variable name will unexpectedly modify that last element — a bug that’s very hard to track down.
foreach with Objects
#
foreach works on any object implementing the Traversable interface, including Iterator and IteratorAggregate:
<?php
// A plain object — foreach iterates over public properties
class Config
{
public string $host = 'localhost';
public int $port = 3306;
public string $dbname = 'mydb';
private string $password = 'secret'; // private — not iterated
}
$config = new Config();
foreach ($config as $key => $value) {
echo "$key: $value\n";
}
// host: localhost
// port: 3306
// dbname: mydb
// (password doesn't appear — private)
// ArrayObject — an array that can be used like an object
$ao = new ArrayObject(['a' => 1, 'b' => 2, 'c' => 3]);
foreach ($ao as $key => $val) {
echo "$key => $val\n";
}
break and continue — Controlling Loop Flow
#
break — Exit the Loop
#
break stops the loop entirely and continues execution after the loop block:
<?php
// Find the first element that meets the criteria
$products = ['laptop', 'mouse', 'keyboard', 'monitor', 'webcam'];
$target = null;
foreach ($products as $item) {
if (str_contains($item, 'key')) {
$target = $item;
break; // stop as soon as it's found, no need to iterate the rest
}
}
echo $target; // "keyboard"
// break with a numeric argument — exit N levels of nested loops
$matrix = [[1,2,3],[4,5,6],[7,8,9]];
$search = 5;
$found = false;
foreach ($matrix as $row => $cols) {
foreach ($cols as $col => $value) {
if ($value === $search) {
echo "Found at row $row, col $col\n";
$found = true;
break 2; // exit BOTH loops at once
}
}
}
continue — Skip This Iteration
#
continue skips the rest of the current iteration and jumps straight to the next one:
<?php
// Process only valid items, skip invalid ones
$orders = [
['id' => 1, 'total' => 150000, 'status' => 'active'],
['id' => 2, 'total' => 0, 'status' => 'active'], // skip
['id' => 3, 'total' => 75000, 'status' => 'cancelled'],// skip
['id' => 4, 'total' => 200000, 'status' => 'active'],
];
$totalRevenue = 0;
foreach ($orders as $order) {
if ($order['total'] <= 0) {
continue; // skip orders with 0 or negative totals
}
if ($order['status'] !== 'active') {
continue; // skip non-active orders
}
$totalRevenue += $order['total'];
}
echo "Total revenue: Rp " . number_format($totalRevenue); // 350.000
// continue with an argument — jump to the outer loop's iteration
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
if ($j === 1) {
continue 2; // skip the outer loop's iteration (not just the inner one)
}
echo "($i,$j) ";
}
}
// Output: (0,0) (1,0) (2,0) — columns 1 and 2 are never printed
break vs continue vs return
#
<?php
// break — stop the loop, continue after the loop
// continue — skip this iteration, move to the next
// return — exit the FUNCTION (which also stops the loop)
function findUser(array $users, int $targetId): ?array
{
foreach ($users as $user) {
if ($user['id'] === $targetId) {
return $user; // return immediately — no flag or break needed
}
}
return null;
}
// return inside a loop is the cleanest way to "find and return"
// because it needs no extra flag variable
Alternative Syntax for Templates #
Like if, foreach and for also have alternative syntax that’s cleaner in HTML template files:
<!-- foreach with alternative syntax -->
<ul>
<?php foreach ($products as $item): ?>
<li>
<strong><?= htmlspecialchars($item['name']) ?></strong>
— Rp <?= number_format($item['price']) ?>
</li>
<?php endforeach; ?>
</ul>
<!-- for with alternative syntax -->
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?page=<?= $i ?>" class="<?= $i === $currentPage ? 'active' : '' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
<!-- while with alternative syntax -->
<?php while ($row = $stmt->fetch()): ?>
<tr>
<td><?= $row['id'] ?></td>
<td><?= htmlspecialchars($row['name']) ?></td>
</tr>
<?php endwhile; ?>
Generators — Iterating Large Data Efficiently #
For very large data (files with millions of lines, database queries producing thousands of rows), loading all the data into an array in memory before iterating can cause memory exhaustion. Generators let you produce values one at a time using the yield keyword:
<?php
// ANTI-PATTERN: read the whole file into an array first — wasteful memory
function readFileLegacyV1(string $path): array
{
return file($path); // loads ALL lines into memory at once
}
foreach (readFileLegacyV1('large_data.csv') as $line) {
process($line);
}
// If the file is 1GB → PHP needs 1GB+ of RAM
// CORRECT: generator — only one line in memory at any time
function readFileGenerator(string $path): Generator
{
$handle = fopen($path, 'r');
if ($handle === false) {
throw new \RuntimeException("Failed to open: $path");
}
try {
while (($line = fgets($handle)) !== false) {
yield trim($line); // produce one line, pause, wait for next()
}
} finally {
fclose($handle); // always close, even on exception
}
}
// Usage is identical to a normal array — you can use foreach
foreach (readFileGenerator('large_data.csv') as $number => $line) {
processCSV($line);
// Only one line is in memory at a time — no matter the file size
}
Generators for Large Ranges #
<?php
// range(1, 1000000) creates a 1-million-element array in memory
// A generator produces one value each time it's requested
function rangeGenerator(int $start, int $end, int $step = 1): Generator
{
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
// Iterate 1 million numbers without creating a 1-million-element array
$total = 0;
foreach (rangeGenerator(1, 1_000_000) as $number) {
$total += $number;
}
echo $total; // 500000500000
// Generator with key-value
function inventoryGenerator(PDO $db): Generator
{
$stmt = $db->query("SELECT id, name, stock FROM products");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row['id'] => $row; // key => value
}
}
foreach (inventoryGenerator($db) as $id => $product) {
if ($product['stock'] < 10) {
echo "Low stock: {$product['name']} ($id)\n";
}
}
Loops vs Array Functions — When to Use Which #
PHP has a range of array functions that perform iteration internally. They’re often cleaner and more expressive than explicit loops:
<?php
$products = [
['name' => 'Laptop', 'price' => 15000000, 'stock' => 5],
['name' => 'Mouse', 'price' => 250000, 'stock' => 0],
['name' => 'Monitor', 'price' => 5000000, 'stock' => 12],
['name' => 'Keyboard','price' => 800000, 'stock' => 3],
];
// ANTI-PATTERN: explicit loops for simple transformations/filters
$productNames = [];
foreach ($products as $p) {
$productNames[] = $p['name'];
}
$available = [];
foreach ($products as $p) {
if ($p['stock'] > 0) {
$available[] = $p;
}
}
$totalValue = 0;
foreach ($products as $p) {
$totalValue += $p['price'] * $p['stock'];
}
// CORRECT: array functions are more concise and expressive
$productNames = array_column($products, 'name');
$available = array_filter($products, fn($p) => $p['stock'] > 0);
$totalValue = array_reduce($products, fn($carry, $p) => $carry + ($p['price'] * $p['stock']), 0);
// Transformation with array_map
$pricesWithVat = array_map(
fn($p) => [...$p, 'price_vat' => $p['price'] * 1.11],
$products
);
A guide to choosing between explicit loops and array functions:
Use array functions (array_map, array_filter, array_reduce) when:
✓ Simple transformation: change every element into another form (map)
✓ Filter: select a subset of elements based on a condition (filter)
✓ Aggregation: compute totals, averages, joins (reduce)
✓ The result is a new array — doesn't modify the original
Use explicit loops when:
✓ The logic is too complex for a single arrow function expression
✓ You need break/continue — early exit based on a condition
✓ Side effects: operations like DB inserts, sending emails, writing files
✓ You need access to state from previous iterations
✓ Multiple different operations on one element in a single pass
Summary #
whilefor conditions unknown in advance — the condition is checked at the start, and the block may never run. Good for reading streams, polling, and retry loops.do...whilefor blocks that must execute at least once — the condition is checked at the end. Most idiomatic for input validation and pagination.forwhen the iteration count is known and you need explicit numeric index control — including backward loops, steps other than 1, or two indices at once.foreachis the most idiomatic way to iterate arrays and iterables — avoidforwith manual indices whenforeachis sufficient.foreachworks on a copy — modifying$iteminside the loop doesn’t change the array. Use&$itemfor direct modification, and alwaysunset($item)afterwards to avoid subtle reference bugs.break Nandcontinue Nlet you exit or skip N levels of nested loops at once.- Generators (
yield) are the solution for iterating large data — only one element is in memory at a time, without loading the whole dataset into an array.- Array functions (
array_map,array_filter,array_reduce,array_column) are more concise and expressive than explicit loops for simple transformations, filters, and aggregations.