PHP provides foreach for iterables, for for explicit counters, while for pre-checked state, and do-while for one required first execution. Every loop still needs initialization, a precise continuation rule, progress, and a bounded termination policy.
Reliable loops preserve collection ownership, use break and continue visibly, batch external work, stream large inputs, and test empty, boundary, retry, mutation, and partial-failure paths.
Use key => value when the array key carries meaning. Iterating by reference can modify the original array, but unset the reference variable afterward to prevent later assignments from changing the last item.
<?php
$progress = ['syntax' => 100, 'forms' => 60, 'pdo' => 20];
foreach ($progress as $topic => $percent) {
echo "{$topic}: {$percent}%" . PHP_EOL;
}
syntax: 100%
forms: 60%
pdo: 20%
A for loop keeps initialization, condition, and update together. It fits an index or an exact repetition count.
<?php
for ($page = 1; $page <= 3; $page++) {
echo "Page {$page}" . PHP_EOL;
}
Page 1
Page 2
Page 3
Use while when the number of attempts is not known in advance. The body must move the condition toward false or deliberately break.
<?php
$attempt = 1;
$connected = false;
while (!$connected && $attempt <= 3) {
echo "Attempt {$attempt}" . PHP_EOL;
$connected = $attempt === 2;
$attempt++;
}
Attempt 1
Attempt 2
break exits the loop. continue skips the rest of the current iteration. Keep either statement close to the condition it serves.
<?php
$emails = ['maya@example.com', 'invalid', 'lee@example.com'];
foreach ($emails as $email) {
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
continue;
}
echo $email . PHP_EOL;
}
maya@example.com
lee@example.com
| Symptom | Cause to inspect | Correction |
|---|---|---|
| Loop never ends | Condition never changes | Update state or add a justified limit |
| First or last item missing | Boundary uses < versus <= | Write the expected index range |
| Original array changed | foreach used a reference | Avoid the reference or unset it |
| Slow request | Unbounded data or repeated I/O | Limit input and batch external work |
Choose a loop from the shape of the work. Use `foreach` for values from an array or Traversable object, `for` for a counter with a known progression, `while` when continuation depends on changing state, and `do-while` only when one first attempt is required before the test.
Every loop has initialization, a continuation decision, a body, and progress toward termination even when those parts are written in different places. State each part before coding. This exposes missing updates and off-by-one boundaries early.
PHP converts the loop condition to boolean. Avoid relying on loose truthiness when zero, an empty string, or an empty array is valid state. Write a comparison that names the actual boundary, such as `$attempt < $maxAttempts`.
An unbounded loop is a resource policy, not merely syntax. Requests, queue workers, file readers, and paginated clients need record, byte, attempt, or time limits plus a defined result when the limit is reached.
`foreach` accepts arrays and Traversable objects. The key form preserves each source key, while the value-only form ignores it. Iterating an unsupported or uninitialized value is an error; validate nullable external data before the loop instead of suppressing the diagnostic.
Normal value iteration assigns the current value to the loop variable. Objects inside an array still refer to objects, so mutating an object changes that object even without an ampersand. Reassigning the loop variable does not replace an array element during ordinary value iteration.
Reference iteration with `foreach ($items as &$item)` permits element replacement. The loop variable remains a reference to the final element afterward. Call `unset($item)` immediately after the loop, or a later assignment may silently overwrite that element.
Destructuring can unpack known row shapes, but missing positions produce diagnostics. Validate imported CSV, JSON, and database rows before destructuring. Use named domain objects when the record shape is fixed and important.
A foreach loop does not advance the array internal pointer used by `current()` and `key()`. Do not mix pointer functions and foreach as two competing traversal mechanisms; one explicit iteration model is easier to reason about.
A `for` loop evaluates initialization once, checks its condition before each iteration, and runs its update after each completed body. `continue` still leads to the update expression, which matters when progress is kept in the loop header.
Translate the intended range into plain language before choosing `<` or `<=`: "indexes zero through count minus one" differs from "page numbers one through count." Prefer `foreach` when the counter exists only to index every array element.
All three expressions may contain comma-separated operations or be empty, but compact syntax is not automatically clear syntax. Keep independent state changes in the body and make an intentionally infinite loop obvious with an explicit exit condition.
Do not repeatedly call a costly function in the condition unless its result must change every iteration. Capture stable counts, configuration, or parsed values once. Conversely, do not cache a condition whose changing result is the reason the loop terminates.
`while` checks its condition before the body, so it can run zero times. It fits reading until end-of-file, retrying until success or a limit, and processing while a queue has approved work. The body must update or observe state that can make the condition false.
`do-while` checks after the body and therefore runs at least once. It fits a first prompt, first fetch, or first calculation whose result determines repetition. Do not use it when the initial action may be forbidden or unnecessary.
Retries need more than a loop counter. Classify retryable failures, apply a deadline, delay repeated attempts where appropriate, stop on permanent errors, and make repeated operations idempotent or safely deduplicated.
Polling loops should release resources between attempts and honor cancellation or process shutdown. A tight empty loop consumes CPU while learning nothing. Prefer event-driven APIs when the platform offers them.
<?php
$attempt = 0;
do {
$attempt++;
$accepted = $attempt >= 2;
echo "Attempt {$attempt}" . PHP_EOL;
} while (!$accepted && $attempt < 3);
Attempt 1
Attempt 2
The body runs once before the condition is checked, while the maximum prevents unlimited attempts.
`break` ends the current `for`, `foreach`, `while`, `do-while`, or `switch`. `continue` skips the remaining statements in the current iteration and moves to the next loop decision. Place either beside the condition it serves and use braces to keep the control path visible.
Both statements accept a positive numeric level for nested structures. `break 2` exits two enclosing eligible structures; `continue 2` advances the appropriate outer loop. Deep numeric control is difficult to maintain, so extract a function or use a named result when nesting becomes substantial.
A `switch` participates in these level counts. Bare `continue` inside a switch behaves like leaving that switch and raises a warning because it is commonly mistaken for continuing an outer loop. Use explicit structure and tests when switch and loops are nested.
Early `continue` can keep the main path flat by rejecting invalid rows first. Early `break` is appropriate after a match or limit. Neither should conceal partial writes; complete or roll back one unit of work before changing loop control.
Changing the collection being traversed can skip, repeat, or unexpectedly include elements depending on the data type and operation. Build a new result for filtering, queue mutations explicitly, or document a tested mutation contract rather than relying on incidental traversal behavior.
Do not execute one database query or remote request per element when a batch API or joined query can perform the work once. This N-plus-one pattern scales with the row count and often dominates the cost of the loop itself.
Transactions should match the required atomic unit. One transaction per item allows partial progress; one transaction for the entire batch gives all-or-nothing behavior but holds resources longer. Choose deliberately and record failed item identifiers safely.
Generators and iterators let a loop consume large results incrementally. Streaming reduces peak memory only if downstream code also avoids collecting everything. Close handles and cursors through explicit ownership, including exception paths.
Test empty input, one item, the last valid item, an immediate break, every item skipped, maximum iterations, malformed rows, and a failure halfway through. Assert final state and side effects, not only printed output.
For counters, test the exact values visited. For retries, use a fake dependency that succeeds or fails on controlled attempts. For time limits, inject a clock where practical so the suite remains deterministic and fast.
Property-based tests can check that mapping preserves count, filtering returns only accepted values, and batching processes each identifier at most once. A timeout test should still have a hard external ceiling so a defect cannot hang the suite.
Profile with representative collection sizes and dependency latency. Micro-optimizing loop syntax rarely matters beside repeated I/O, accidental copying, quadratic searches, or unbounded memory growth.
0 of 2 checked
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.