PHP (8.5) Module 3 – Control flow
if, else, elseif and the ternary operator
Branch with PHP's if, elseif and else, return early with guard clauses, write the ternary operator safely and use the colon syntax in HTML templates.
What you will learn
- Write if, elseif and else chains that test their conditions in the right order
- Refactor nested conditions into guard clauses that return early
- Use the ternary operator, and parenthesise or replace a nested one
- Write template branches in the colon syntax, with elseif in one word
- Avoid the precedence trap of and and or in assignments
Before you start
What you’ll learn · Before you start
What you will learn
- Write if, elseif and else chains that test their conditions in the right order
- Refactor nested conditions into guard clauses that return early
- Use the ternary operator, and parenthesise or replace a nested one
- Write template branches in the colon syntax, with elseif in one word
- Avoid the precedence trap of and and or in assignments
Before you start
On this page
A shop delivers free once an order reaches 999 rupees, and otherwise charges 40 rupees in a metro city and 70
elsewhere. That makes three rules, and for every order PHP must run exactly one of them: the job of if, elseif and
else, which test their conditions from the top and run the first block whose condition is true.
This lesson writes that choice in four forms and runs each on PHP 8.5.11: an elseif chain, guard clauses that return
early, the ternary operator where one of two values is enough, and the colon syntax of HTML templates. It also shows
three mistakes that look right: tests in the wrong order, a nested ternary without brackets, which PHP 8 refuses to
compile, and and written where && was meant.
elseif tests conditions in order, and the first true one wins
deliveryFee() below decides the fee in one chain. PHP evaluates the condition after if and turns it into a bool,
as the lesson on booleans showed. When it is true, PHP runs that block and skips every elseif and else after it.
When it is false, PHP moves down to the next elseif, and else, which has no condition, runs only when nothing
above it was true. Step through four orders and watch which test decides each one:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Order 1: 500.00, metro. deliveryFee(50_000, true) starts at the top. The if asks whether 50,000 paise is at least 99,900. It is not, so PHP skips the block under the if.
- Order 1: the elseif. PHP moves down to the elseif and tests $isMetro. This order goes to a metro city, so the condition is true.
- Order 1: fee 40.00. The first true condition wins: $fee becomes 4,000 paise, 40 rupees. PHP skips the else without looking at it and leaves the chain.
- Order 2: 998.99, non-metro. deliveryFee(99_899, false) is one paisa short of the free-delivery mark, so the if is false, and $isMetro is false too.
- Order 2: the else runs. No condition above it was true, so the else runs: $fee becomes 7,000 paise, 70 rupees. The else has no condition of its own.
- Order 3: 999.00, non-metro. deliveryFee(99_900, false) is exactly on the mark. >= includes the boundary, so the first test is true and $fee becomes 0.
- Order 4: 1,500.00, metro. deliveryFee(150_000, true) makes both the first test and $isMetro true. Only the first true branch runs: the fee is 0, and the elseif is never even tested.
The program behind the figure prints the same four orders:
<?php
// Amounts are whole paise (100 paise = 1 rupee). Orders of 999 rupees or more ship free.
function deliveryFee(int $orderPaise, bool $isMetro): int
{
if ($orderPaise >= 99_900) {
$fee = 0;
} elseif ($isMetro) {
$fee = 4_000;
} else {
$fee = 7_000;
}
return $fee;
}
function rupees(int $paise): string
{
return number_format(intdiv($paise, 100)) . sprintf('.%02d', $paise % 100);
}
function show(int $orderPaise, bool $isMetro, string $city): void
{
printf("Order %8s, %-10s fee %5s\n", rupees($orderPaise), $city, rupees(deliveryFee($orderPaise, $isMetro)));
}
show(50_000, true, 'metro:');
show(99_899, false, 'non-metro:');
show(99_900, false, 'non-metro:');
show(150_000, true, 'metro:'); Output
Order 500.00, metro: fee 40.00 Order 998.99, non-metro: fee 70.00 Order 999.00, non-metro: fee 0.00 Order 1,500.00, metro: fee 0.00
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php delivery_fee.php
The last order, 1,500 rupees to a metro city, satisfies both the first test and $isMetro, and its fee is 0.00,
because only the first true branch runs. That rule makes the order of the tests part of the logic: when one condition
is wider than another, the narrow one must come first. This program asks the same two discount questions in both
orders:
<?php
// 10 % off from 5,000 rupees, 5 % off from 2,000 rupees (amounts in paise).
function discountRight(int $totalPaise): int
{
if ($totalPaise >= 500_000) {
$percent = 10;
} elseif ($totalPaise >= 200_000) {
$percent = 5;
} else {
$percent = 0;
}
return $percent;
}
// The same tests, the wider one first.
function discountWrong(int $totalPaise): int
{
if ($totalPaise >= 200_000) {
$percent = 5;
} elseif ($totalPaise >= 500_000) {
$percent = 10;
} else {
$percent = 0;
}
return $percent;
}
function show(int $totalPaise): void
{
printf(
"%8s rupees: right order %2d %%, wrong order %2d %%\n",
number_format(intdiv($totalPaise, 100)),
discountRight($totalPaise),
discountWrong($totalPaise),
);
}
show(150_000);
show(200_000);
show(499_900);
show(500_000);
show(800_000); Output
1,500 rupees: right order 0 %, wrong order 0 % 2,000 rupees: right order 5 %, wrong order 5 % 4,999 rupees: right order 5 %, wrong order 5 % 5,000 rupees: right order 10 %, wrong order 5 % 8,000 rupees: right order 10 %, wrong order 5 %
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php discount_order.php
With >= 200_000 first, every total of 5,000 rupees or more stops at the 5 % branch, so the 10 % branch can never
run, and PHP gives no warning about it. The two versions agree below 5,000 rupees and differ from exactly 5,000 up,
which is why tests belong at the boundaries: one value just below each threshold and one exactly on it.
An else that belongs to another if
Without braces, an else belongs to the nearest if, whatever the indentation says. In
if ($a) if ($b) echo 'b'; else echo 'c'; the else is part of if ($b), so nothing prints when $a is false.
PSR-12 asks for braces around every block, which removes the question.
Checkpoint
Check: if, elseif and else, in order
Guard clauses return early instead of nesting
Rules pile up. Once the shop also refuses orders of 0 paise, a natural first draft puts one if inside another, and
the fee ends up three levels deep. A guard clause: An if at the top of a function that handles one special case, such as an invalid input, and leaves at once with return or throw, so the main rule can follow without nesting. handles one special case at the top
of the function and leaves at once with return or throw, so the main rule comes last, with no nesting at all.
Both versions below make the same four calls; compare their shape:
Nested · fee_nested.php
<?php
function deliveryFee(int $orderPaise, bool $isMetro): int
{
if ($orderPaise > 0) {
if ($orderPaise < 99_900) {
if ($isMetro) {
return 4_000;
} else {
return 7_000;
}
} else {
return 0;
}
} else {
throw new InvalidArgumentException("An order must cost more than 0 paise, got $orderPaise");
}
}
echo deliveryFee(99_899, true), "\n";
echo deliveryFee(99_899, false), "\n";
echo deliveryFee(99_900, false), "\n";
echo deliveryFee(0, true), "\n"; Output (exit status 255)
4000
7000
0
Fatal error: Uncaught InvalidArgumentException: An order must cost more than 0 paise, got 0 in fee_nested.php:16
Stack trace:
#0 fee_nested.php(23): deliveryFee(0, true)
#1 {main}
thrown in fee_nested.php on line 16
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php fee_nested.php
Guard clauses · fee_guards.php
<?php
function deliveryFee(int $orderPaise, bool $isMetro): int
{
if ($orderPaise <= 0) {
throw new InvalidArgumentException("An order must cost more than 0 paise, got $orderPaise");
}
if ($orderPaise >= 99_900) {
return 0;
}
if ($isMetro) {
return 4_000;
}
return 7_000;
}
echo deliveryFee(99_899, true), "\n";
echo deliveryFee(99_899, false), "\n";
echo deliveryFee(99_900, false), "\n";
echo deliveryFee(0, true), "\n"; Output (exit status 255)
4000
7000
0
Fatal error: Uncaught InvalidArgumentException: An order must cost more than 0 paise, got 0 in fee_guards.php:6
Stack trace:
#0 fee_guards.php(21): deliveryFee(0, true)
#1 {main}
thrown in fee_guards.php on line 6
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php fee_guards.php
Both printed 4000, 7000 and 0, then stopped at the fourth call with the same InvalidArgumentException: the refusal
of an empty order. In the nested version the throw sits at the bottom, far from the test it answers, and the else
branches push every rule further right. In the guard version each rule is one if with its result beside it. An
else after a return is never needed, because the function has already left by then.
The flat version is also easier to test. Each guard is one boundary: 0 paise, 99,900 paise and 99,899 paise to a metro city exercise one guard each, and 99,899 paise anywhere else reaches the last line.
Key idea
Handle the special cases first and leave the function as soon as its answer is known: what is left at the bottom is the normal case, read without any nesting.
Checkpoint
Check: Guard clauses that return early
The ternary operator picks one of two values
When each branch only produces a value, a whole if and else block is more than you need. The
ternary operator: The operator condition ? a : b, an expression that evaluates to a when the condition is true and to b otherwise. Its short form a ?: b gives a itself when a counts as true. condition ? a : b is an
expression: Code that produces a value, such as 2 + 3, $price * 2, a function call or a ternary. A statement such as if or echo does something instead, so it cannot stand on the right of =.: it evaluates to a when the condition is true and to b otherwise, so it
can stand inside an echo or on the right of =. Its short form a ?: b gives a itself when a counts as true,
which suits a default for an empty name:
<?php
$items = 1;
echo "$items ", $items === 1 ? 'item' : 'items', " in your cart\n";
$items = 3;
echo "$items ", $items === 1 ? 'item' : 'items', " in your cart\n";
$nickname = '';
echo 'Hello, ', $nickname ?: 'guest', "\n";
$stock = 0;
$label = $stock === 0 ? 'Sold out' : ($stock < 5 ? 'Low stock' : 'In stock');
echo $label, "\n"; Output
1 item in your cart 3 items in your cart Hello, guest Sold out
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php ternary.php
The ternary and the two shorthands that look like it differ in what they test:
| Written | Gives | Falls back when the left side is |
|---|---|---|
$cond ? $a : $b |
$a or $b |
$cond is false (any falsy value) |
$a ?: $b |
$a or $b |
$a is falsy: 0, '', '0', null, [], false |
$a ?? $b |
$a or $b |
$a is null or does not exist, with no warning |
Stacking ternaries is where PHP differs from most languages. Before PHP 8.0, a chain without brackets grouped from the
left, so a ? b : c ? d : e meant (a ? b : c) ? d : e, which few readers expect. PHP 7.4 deprecated the form, and
PHP 8.0 refuses to compile it:
<?php
$stock = 0;
$label = $stock === 0 ? 'Sold out' : $stock < 5 ? 'Low stock' : 'In stock';
echo $label, "\n"; Output (exit status 255)
Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported. Use either `(a ? b : c) ? d : e` or `a ? b : (c ? d : e)` in nested_ternary.php on line 4
Stack trace:
#0 {main}
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php nested_ternary.php
The message names both fixes. The last line of ternary.php used the second one, brackets around the inner ternary,
and printed Sold out. Nothing of nested_ternary.php ran, not even the line before the faulty one, because PHP found the
mistake while compiling the file. For three or more outcomes a match expression, the subject of the next lesson,
reads better than any chain of ternaries.
Nested ternaries in older code
PHP 7.4 deprecated nested ternaries without brackets, and PHP 8.0 made the operator non-associative, so such code no longer compiles. Code written for PHP 7.3 or older may still contain them: add the brackets the message suggests, in the grouping the code meant.
Checkpoint
Check: The ternary operator and nesting
Templates use the colon syntax, with elseif in one word
In a file that is mostly HTML, closing braces get lost between the tags. PHP’s
alternative syntax: The colon form of if, while, for, foreach and switch that templates use: the opening brace becomes a colon, and the closing brace becomes endif;, endwhile;, endfor;, endforeach; or endswitch;. replaces the opening brace with a colon and the closing brace
with a keyword that names the block: endif;, endforeach;, endwhile;, endfor; or endswitch;. This template
prints a card for each of three products. foreach, which the loops lesson of this module covers, repeats the card,
and the if inside it picks one of three lines:
<?php
$products = [
['name' => 'Steel bottle', 'price' => 54_900, 'stock' => 0, 'offer' => 0],
['name' => 'Desk lamp', 'price' => 129_900, 'stock' => 8, 'offer' => 10],
['name' => 'Notebook & pen', 'price' => 24_900, 'stock' => 40, 'offer' => 0],
];
function rupees(int $paise): string
{
return number_format(intdiv($paise, 100)) . sprintf('.%02d', $paise % 100);
}
?>
<?php foreach ($products as $p): ?>
<article class="card">
<h2><?= htmlspecialchars($p['name']) ?></h2>
<?php if ($p['stock'] === 0): ?>
<p class="status">Sold out</p>
<?php elseif ($p['offer'] > 0): ?>
<p class="price"><?= rupees(intdiv($p['price'] * (100 - $p['offer']), 100)) ?> (<?= $p['offer'] ?> % off)</p>
<?php else: ?>
<p class="price"><?= rupees($p['price']) ?></p>
<?php endif; ?>
</article>
<?php endforeach; ?> Output
<article class="card"> <h2>Steel bottle</h2> <p class="status">Sold out</p> </article> <article class="card"> <h2>Desk lamp</h2> <p class="price">1,169.10 (10 % off)</p> </article> <article class="card"> <h2>Notebook & pen</h2> <p class="price">249.00</p> </article>
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php cards.php
Each card took exactly one branch: the bottle is sold out, the lamp shows its offer price and the notebook its plain
price. <?= … ?> prints a value, and htmlspecialchars() turned the & of “Notebook & pen” into &, so a
browser shows it as text rather than reading it as markup. One rule of the colon syntax catches people who learned the
brace form first:
<?php $stock = 0; ?>
<?php if ($stock > 5): ?>
<p>In stock</p>
<?php else if ($stock > 0): ?>
<p>Low stock</p>
<?php else: ?>
<p>Sold out</p>
<?php endif; ?> Output (exit status 255)
Parse error: syntax error, unexpected token "if", expecting ":" in colon_else_if.php on line 4
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php colon_else_if.php
With braces, else if is an else whose statement happens to be another if, and it behaves exactly like elseif.
In the colon syntax PHP expects else: and finds if, so the file does not compile. Write elseif in one word in
both syntaxes, as PSR-12 recommends, and never mix braces and colons in one block, which the manual says is not
supported.
Checkpoint
Check: The colon syntax in templates
and, or and xor bind after the assignment
PHP has two spellings for two of its logical operators: && and and, || and or. They compute the same truth
values but sit at very different places in the precedence table, and the gap matters as soon as an assignment is in
the line:
<?php
$inStock = true;
$addressOk = false;
$canShip = $inStock and $addressOk;
var_dump($canShip);
$canShip = $inStock && $addressOk;
var_dump($canShip);
$canShip = ($inStock and $addressOk);
var_dump($canShip); Output
bool(true) bool(false) bool(false)
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php and_or.php
The first var_dump() printed bool(true) although the address check failed.
operator precedence: The order in which PHP groups the operators of an expression: an operator that binds more tightly takes its operands first, as * does before +, and = does before and. decides which operator takes its operands first, and =
comes before and, so PHP ran ($canShip = $inStock) and $addressOk: it stored true, then computed true and false
and threw that result away. && comes before =, so the second line stored the value of the whole condition,
false, and so did the third, where brackets set the grouping by hand. The operators of this lesson, in the order they
bind:
| Order | Operators, from the first to bind to the last |
|---|---|
| 1 | ! |
| 2 | comparisons such as <, >=, == and === |
| 3 | &&, then || |
| 4 | ??, then ? : |
| 5 | = and the assignments such as += |
| 6 | and, then xor, then or |
The manual’s own example shows the same with or: after $f = false or true; the variable holds false. Use && and
|| in conditions and assignments. or survives mostly in the old idiom $file = fopen($path, 'r') or die('…');,
which works only because the assignment happens first and or then looks at what was stored. Lines like that one are
why PHP-CS-Fixer, whose logical_operators rule rewrites and and or as && and ||, marks the rule as risky:
check each line it changes.
Checkpoint
Check: and, or and operator precedence
Practise: a delivery fee you can test
Add a remote-area rule to deliveryFee()
On your own computer, copy the guard-clause version of deliveryFee() from the tabs above and give it a third
parameter, bool $isRemote: an order to a remote area pays 12,000 paise, whatever its total. Decide where the new
guard goes, then check it with php -n: 150,000 paise to a remote area must cost 12,000, 150,000 to a metro city 0,
99,899 to a metro city 4,000, and 0 paise must still throw. Then move the new guard below the test for 99,900 paise
and run it again: the remote order now costs 0, and PHP prints no warning.
Finish with the self-check. It opens on whichever of the five ideas you have shown least so far, and a wrong pick names the slip behind it:
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher · Warm-up
Are elseif and else if the same in PHP?
With braces they behave the same. else if is an else whose statement happens to be another if, while elseif is one keyword, and both run the first branch whose condition is true and skip the rest. They differ in the colon syntax of templates: after if (...): PHP accepts elseif (...):, but else if (...): is a parse error, "unexpected token "if", expecting ":"", because after else PHP expects the colon that opens the else block. Since one file may use both syntaxes, write elseif in one word everywhere. PSR-12 recommends it so that every control keyword is a single word, and it means code moved into a template keeps compiling.
-
Fresher to mid
A checkout function prices delivery with if statements nested five levels deep: member or not, coupon or not, metro or not, order size, weekend or not. Members sometimes pay the non-member price. How do you find the cause, and how do you restructure the function so that the next mistake is easy to see?
-
Fresher to mid
Reading a signup handler, you find $valid = $hasEmail and $hasPhone;, and its tests pass. What does the line really store, why did the tests miss it, and how do you find and fix similar lines in a large codebase?
Key takeaways
if,elseifandelsetest their conditions from the top and run only the first block whose condition is true, so a narrow condition must come before a wider one.- Guard clauses handle the special cases first and return or throw at once, which leaves the main rule at the bottom, without nesting, and gives each boundary its own test.
- The ternary operator is an expression that picks one of two values; nested ternaries need brackets since PHP 8.0,
and
matchreads better for three or more outcomes. - Templates use the colon syntax with
endif;and its relatives, where only the one-wordelseifcompiles. andandorbind after=, so$x = $a and $b;stores$a; conditions and assignments use&&and||.
Terms in this lesson
- alternative syntax
- The colon form of if, while, for, foreach and switch that templates use: the opening brace becomes a colon, and the closing brace becomes endif;, endwhile;, endfor;, endforeach; or endswitch;.
- expression
- Code that produces a value, such as 2 + 3, $price * 2, a function call or a ternary. A statement such as if or echo does something instead, so it cannot stand on the right of =.
- guard clause
- An if at the top of a function that handles one special case, such as an invalid input, and leaves at once with return or throw, so the main rule can follow without nesting.
- operator precedence
- The order in which PHP groups the operators of an expression: an operator that binds more tightly takes its operands first, as * does before +, and = does before and.
- ternary operator
- The operator condition ? a : b, an expression that evaluates to a when the condition is true and to b otherwise. Its short form a ?: b gives a itself when a counts as true.
References
- PHP Manual: if (The PHP Group (php.net))
- PHP Manual: else (The PHP Group (php.net))
- PHP Manual: elseif/else if (The PHP Group (php.net))
- PHP Manual: Alternative syntax for control structures (The PHP Group (php.net))
- PHP Manual: Comparison operators (ternary operator) (The PHP Group (php.net))
- PHP Manual: Operator precedence (The PHP Group (php.net))
- PHP Manual: Logical operators (The PHP Group (php.net))
- PHP 7.4 migration guide: deprecated features (The PHP Group (php.net))
- PHP Manual: Booleans (converting to boolean) (The PHP Group (php.net))
- PSR-12: Extended Coding Style (control structures) (PHP Framework Interop Group (PHP-FIG))
- PHP Manual: htmlspecialchars (The PHP Group (php.net))
- PHP-CS-Fixer rules: logical_operators (PHP-CS-Fixer (cs.symfony.com))
Report a problem with this lesson
Kept only in this browser. Your Learn progress