Control Flow and Loops
Making decisions and repeating work in PHP: if, elseif and else, switch versus match, while, for and foreach, and break and continue.
if, elseif and else
An if statement runs a block of code only when its
condition is true. The condition goes in parentheses and the block
in braces, as in JavaScript and C. elseif adds further
conditions, tested from top to bottom until one is true, and
else catches everything that is left. Only the first
matching branch runs.
<?php
$kwh = 1542.7;
if ($kwh > 2000) {
echo "High usage\n";
} elseif ($kwh > 1000) {
echo "Normal usage\n";
} else {
echo "Low usage\n";
}
$open = 0;
if ($open === 0) echo "No open tickets\n"; // one statement: braces optional
Normal usage No open tickets
Braces are optional when a branch has one statement, as in the last
line, but most coding standards require them anyway: adding a second
line later without adding braces is a classic bug. PHP accepts both
elseif and else if; in normal code they
behave the same, but only elseif works in the
alternative syntax.
Truthy and falsy values
A condition does not have to be a boolean. PHP converts the value,
and a short list of values counts as false: false,
0, 0.0, the empty string '',
the string '0', null and the empty array.
Everything else is true. The idea is the same as Python's
truthy and falsy
values, but the lists differ.
<?php
$values = [
'0' => 0, '0.0' => 0.0, "''" => '', "'0'" => '0', 'null' => null, '[]' => [],
"'0.0'" => '0.0', "' '" => ' ', "'false'" => 'false', '[0]' => [0],
];
foreach ($values as $label => $value) {
echo str_pad($label, 8), $value ? 'truthy' : 'falsy', "\n";
}
0 falsy 0.0 falsy '' falsy '0' falsy null falsy [] falsy '0.0' truthy ' ' truthy 'false' truthy [0] truthy
Conditional expressions
For choosing between two values, an expression is often shorter than
an if statement. PHP has three:
-
The ternary operator
condition ? a : bgivesawhen the condition is truthy andbotherwise. -
The short ternary
$x ?: $defaultgives$xif it is truthy, otherwise the default. It treats'',0and'0'as missing. -
The
null coalescing operator
$x ?? $defaultgives the default only when$xisnullor does not exist, without a warning. It is the usual way to read optional request parameters.
<?php
$tickets = 3;
echo $tickets === 1 ? 'ticket' : 'tickets', "\n"; // ternary operator
$city = '';
echo $city ?: 'unknown', "\n"; // short ternary: falls back on any falsy value
echo $city ?? 'unknown', "\n"; // null coalescing: only null or missing
$_GET = ['area' => 'Deventer']; // simulated request
echo $_GET['area'] ?? 'all areas', "\n";
echo $_GET['page'] ?? 1, "\n";
tickets unknown Deventer 1
The example fills $_GET by hand to simulate a request
to ?area=Deventer; in a real web request PHP fills it
from the query string (see
PHP and the Web). Do not nest ternary operators without parentheses: since PHP 8.0
that is a compile error, because the old behavior was confusing.
switch
A switch statement compares one value with a list of
case labels and jumps to the first that matches.
Execution then continues until a break, which is why
each case normally ends with one. Several labels in a row share one
block. default runs when no case matches.
<?php
$priority = 2;
switch ($priority) {
case 1:
echo "Call the on-call engineer\n";
break;
case 2:
case 3: // two cases, one block
echo "Handle within the shift\n";
break;
default:
echo "Add to the backlog\n";
}
// Forgetting break: execution falls through into the next case
switch ($priority) {
case 2:
echo "Priority 2\n";
case 3:
echo "Priority 3 (fell through!)\n";
}
Handle within the shift Priority 2 Priority 3 (fell through!)
The second switch shows fall-through:
without break, PHP keeps running the code of the
following cases, whether or not their labels match. Occasionally
that is intended, but far more often it is a forgotten
break. Like the
switch statement
in JavaScript, but unlike JavaScript's strict matching, PHP's
switch compares with loose ==.
match
PHP 8.0 added the
match expression, a stricter
and shorter alternative to switch. It compares the
subject with each arm using strict ===, returns the
value of the first arm that matches, and never falls through.
Several conditions can share one arm, separated by commas. A
trailing comma after the last arm is allowed.
<?php
$status = 'in_progress';
$label = match ($status) {
'new' => 'New',
'in_progress', 'on_hold' => 'Being worked on',
'resolved', 'closed' => 'Done',
default => 'Unknown status',
};
echo $label, "\n";
$kwh = 1542.7;
$band = match (true) {
$kwh > 2000 => 'high',
$kwh > 1000 => 'normal',
default => 'low',
};
echo $band, "\n";
Being worked on normal
The second match uses a common pattern:
match (true) with conditions as arms. PHP evaluates the
conditions from top to bottom and returns the value of the first one
that is true, which makes it a compact replacement for
an if/elseif chain that only chooses a
value. Because the comparison is strict, each condition must really
produce true, not just a truthy value.
switch versus match
The strict comparison matters with input from a form or a URL, which
always arrives as a string. switch happily matches the
string '1' to case 1;
match does not, and when no arm matches and there is no
default, it throws an
UnhandledMatchError instead of silently doing nothing:
<?php
$priority = '1'; // from a form: always a string
switch ($priority) {
case 1:
echo "switch: matched 1 (loose ==)\n";
break;
}
echo match ($priority) {
1 => "match: matched 1\n",
2 => "match: matched 2\n",
};
switch: matched 1 (loose ==) Fatal error: Uncaught UnhandledMatchError: Unhandled match case '1' in example.php:10
That error is a feature: a status value nobody planned for stops the
code loudly instead of producing a wrong label. Convert input first
((int) $priority, or better, validate it), and add a
default arm only when there really is a sensible
fallback.
switch |
match |
|
|---|---|---|
| Comparison | Loose == |
Strict === |
| Kind | Statement: runs blocks | Expression: returns a value |
| Fall-through | Yes, unless you write break |
Never |
| Arm content | Any number of statements | One expression |
| No match, no default | Nothing happens | UnhandledMatchError |
| Available since | Always | PHP 8.0 |
while and do-while
A while loop repeats its block as long as the condition
is true. The condition is tested before each pass, so the
block may not run at all. A do … while loop tests
after each pass, so its block always runs at least once:
useful when you need to try something before you can tell whether to
try again.
<?php
$battery = 100;
$hours = 0;
while ($battery > 20) { // test first: may run zero times
$battery -= 30;
$hours++;
}
echo "Warning after $hours hours ($battery%)\n";
$attempt = 0;
do { // test last: runs at least once
$attempt++;
echo "Connection attempt $attempt\n";
$connected = $attempt === 2;
} while (!$connected && $attempt < 5);
Warning after 3 hours (10%) Connection attempt 1 Connection attempt 2
Make sure something inside the loop changes the condition. A loop
whose condition never becomes false is an
infinite loop. In
a web request PHP stops it after
max_execution_time (usually 30 seconds for web
requests), but on the command line there is no limit by default. The
extra counter in the do … while loop is a safety net:
the loop gives up after five attempts even if the connection never
succeeds.
for loops
A for loop puts three expressions in its head,
separated by semicolons: the start (run once), the condition (tested
before every pass) and the step (run after every pass). It is the
natural choice when you count.
<?php
for ($i = 1; $i <= 3; $i++) {
echo "Reading $i\n";
}
$meters = ['MTR-100231', 'MTR-100232', 'MTR-100233'];
for ($i = 0, $n = count($meters); $i < $n; $i += 2) {
echo $i, ': ', $meters[$i], "\n";
}
Reading 1 Reading 2 Reading 3 0: MTR-100231 2: MTR-100233
Each part of the head may contain several expressions separated by
commas. The second loop uses that to call count() once,
before the loop, instead of on every pass. To walk through an array,
foreach is usually the better choice: it needs no
counter and also works when the keys are not 0, 1, 2.
foreach
The foreach loop visits every
element of an array in order.
foreach ($array as $value) gives you the values;
foreach ($array as $key => $value) gives you the
keys as well. It also works on objects that are iterable, such as
the rows of a database result.
<?php
$usage = ['Zwolle' => 1210, 'Deventer' => 1542, 'Assen' => 980];
foreach ($usage as $kwh) { // values only
echo $kwh, "\n";
}
foreach ($usage as $city => $kwh) { // keys and values
echo "$city: $kwh kWh\n";
}
foreach (['Zwolle', 'Assen'] as $i => $city) {
echo $i + 1, ". $city\n"; // the key of a list is its index
}
1210 1542 980 Zwolle: 1210 kWh Deventer: 1542 kWh Assen: 980 kWh 1. Zwolle 2. Assen
The loop works on the array as it was when the loop started. Adding elements inside the loop does not make it run longer:
<?php
$queue = ['INC-31877', 'INC-31880'];
foreach ($queue as $ticket) {
$queue[] = 'follow-up of ' . $ticket; // the loop runs over a copy
echo "Handling $ticket\n";
}
echo count($queue), " items now\n";
Handling INC-31877 Handling INC-31880 4 items now
Changing elements: foreach by reference
The loop variable is a copy of the element, so assigning to it does
not change the array. To change elements in place, put
& before the variable: $price then
is the element, a reference to it. After the loop, the
variable still refers to the last element, so remove the reference
with unset().
<?php
$prices = ['day' => 0.27, 'night' => 0.21];
foreach ($prices as $period => $price) {
$price = $price * 1.1; // changes the loop variable only
}
print_r($prices);
foreach ($prices as $period => &$price) { // & : $price IS the element
$price = round($price * 1.1, 3);
}
unset($price); // break the reference!
print_r($prices);
Array
(
[day] => 0.27
[night] => 0.21
)
Array
(
[day] => 0.297
[night] => 0.231
)
<?php
$numbers = [1, 2, 3];
foreach ($numbers as &$n) {
$n = $n * 10; // [10, 20, 30]
}
// forgot unset($n) here: $n still points at $numbers[2]
foreach ($numbers as $n) { // each step writes into $numbers[2]
}
print_r($numbers);
Array
(
[0] => 10
[1] => 20
[2] => 20
)
The second loop assigns 10, then 20, then 20 again to
$numbers[2], through the leftover reference, so the
last value is lost. Either call unset($n) right after a
loop by reference, or avoid references altogether and build a new
array, for example with
array_map().
break and continue
continue skips the rest of the current pass and moves
on to the next one. break leaves the loop completely.
Both work in every kind of loop, and break also ends a
switch.
<?php
$readings = [318.4, -1, 296.0, 0, 301.5, 99999, 287.2];
foreach ($readings as $i => $kwh) {
if ($kwh <= 0) {
echo "skip #$i (invalid)\n";
continue; // next iteration
}
if ($kwh > 10000) {
echo "stop at #$i (meter fault)\n";
break; // leave the loop
}
echo "use #$i: $kwh\n";
}
use #0: 318.4 skip #1 (invalid) use #2: 296 skip #3 (invalid) use #4: 301.5 stop at #5 (meter fault)
In nested loops, break and continue affect
the innermost loop. A number after them says how many levels to go
up: break 2 leaves the inner and the outer loop, and
continue 2 jumps to the next pass of the outer loop.
JavaScript uses labels for this (see
labeled statements); Python has no direct equivalent.
<?php
$feeders = [
'F1' => ['ok', 'ok', 'ok'],
'F2' => ['ok', 'fault', 'ok'],
'F3' => ['ok', 'ok', 'ok'],
];
foreach ($feeders as $feeder => $sections) {
foreach ($sections as $i => $state) {
if ($state === 'fault') {
echo "First fault: $feeder section $i\n";
break 2; // leave both loops
}
}
echo "$feeder checked\n";
}
F1 checked First fault: F2 section 1
Nested loops that need break 2 are often easier to read
as a function that returns as soon as
it finds the answer.
Alternative syntax for templates
PHP was designed to be mixed with HTML: everything outside
<?php … ?> tags is sent to the output as it is.
In such a template, braces scattered between HTML tags are hard to
match up. The
alternative syntax
replaces the opening brace with a colon and the closing brace with a
keyword: endif;, endforeach;,
endfor;, endwhile; and
endswitch;. WordPress themes and many other templates
use it.
<?php
$outages = [
['area' => 'Deventer', 'customers' => 412],
['area' => 'Zwolle', 'customers' => 37],
];
?>
<?php if (count($outages) === 0): ?>
<p>No outages.</p>
<?php else: ?>
<ul>
<?php foreach ($outages as $o): ?>
<li><?= htmlspecialchars($o['area']) ?>: <?= $o['customers'] ?> customers</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<ul> <li>Deventer: 412 customers</li> <li>Zwolle: 37 customers</li> </ul>
<?= … ?> is the short echo tag: it prints the
value of one expression. Every value from outside the program, such
as an area name typed into a form, must pass through
htmlspecialchars() before it goes into HTML, or it can
inject markup and scripts (cross-site scripting; see
PHP and the Web). The output also shows a detail of PHP tags: a line break
directly after ?> is swallowed, which is why the
lines with only PHP code leave no empty lines behind.