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 : b gives a when the condition is truthy and b otherwise.
  • The short ternary $x ?: $default gives $x if it is truthy, otherwise the default. It treats '', 0 and '0' as missing.
  • The null coalescing operator $x ?? $default gives the default only when $x is null or 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.