if, elseif, else
A conditional is how a script stops being a fixed document and starts being an application. The same profile.php shows an edit button to the owner and a follow button to everyone else, because one if decides between them.
The structure is straightforward: if takes a condition in brackets and runs the block when it is true; elseif adds further conditions checked in order; else catches everything left. Only the first matching branch runs, which is the property that makes a grading ladder work — once $marks >= 90 matches, none of the lower conditions are even evaluated. Reverse the order and every student gets the lowest grade, because the loosest condition matches first. This is a classic mistake in fee-slab and grade calculations.
PHP accepts both elseif as one word and else if as two, and inside curly braces they behave identically. Pick elseif, because the alternative template syntax discussed later in this lesson requires the single-word form.
One habit worth building early: prefer guard clauses to deep nesting. Instead of wrapping your real work inside four levels of if, check for each failure case at the top and return or exit immediately. The valid path then sits at the left margin where it is easy to read, and each rejection reason is visible on one line.
<?php
$marks = 87;
// Order matters - highest first
if ($marks >= 90) {
$grade = 'A';
} elseif ($marks >= 75) {
$grade = 'B';
} elseif ($marks >= 60) {
$grade = 'C';
} elseif ($marks >= 40) {
$grade = 'D';
} else {
$grade = 'F';
}
echo "Grade: $grade"; // B
// Deep nesting - hard to follow
function saveComment(array $input) {
if (isset($input['body'])) {
if (trim($input['body']) !== '') {
if (mb_strlen($input['body']) <= 500) {
// the actual work, four levels deep
return true;
}
}
}
return false;
}
// Guard clauses - flat, and each rejection is obvious
function saveCommentBetter(array $input): bool {
$body = trim($input['body'] ?? '');
if ($body === '') { return false; }
if (mb_strlen($body) > 500) { return false; }
// the actual work, at the left margin
return true;
} - PHP has no
elifand no Python-style indentation rules. Braces define the block; indentation is only for humans. But do indent consistently anyway — a misleading indent in a long conditional is how people misread code they are debugging at 2 a.m.
What Counts as True
When you write if ($value) without a comparison, PHP converts $value to a boolean using a fixed set of rules. Knowing that list precisely is worth more than it sounds, because the exceptions cause real bugs.
These are the falsy values: the boolean false, the integer 0, the float 0.0, the empty string "", the string "0", the empty array [], and null. Everything else is truthy — including the string "false", the string "0.0", the float -0.5, and an array containing a single zero.
The two that catch people are 0 and "0". Suppose a form asks how many guests a student is bringing, and they honestly answer zero. A check of if ($_POST['guests']) treats that as "nothing was entered" and shows a validation error for a perfectly valid answer. The same thing happens to a house number of 0, a discount of 0, or a search for the digit 0.
The fix is to compare explicitly rather than relying on truthiness. Ask the question you actually mean: if ($guests === '') for "was anything typed", or if ($guests < 0) for "is this a sensible number". Bare truthiness is fine for a value you know is a boolean; for anything that came from outside your program, be explicit.
<?php
// The complete falsy list
foreach ([false, 0, 0.0, "", "0", [], null] as $v) {
var_dump((bool) $v); // all false
}
// Truthy values people assume are falsy
foreach (["false", "0.0", " ", [0], -1, 0.1] as $v) {
var_dump((bool) $v); // all true
}
// The bug
$guests = "0"; // user genuinely entered 0
if (!$guests) {
echo "Please enter the number of guests."; // wrong - they did
}
// The fix: say what you mean
if ($guests === '') {
echo "Please enter the number of guests.";
} elseif (!ctype_digit($guests)) {
echo "Guests must be a whole number.";
} else {
$guests = (int) $guests;
echo "Booking for $guests guests.";
} - A useful discipline: reserve bare
if ($flag)for variables that hold an actual boolean, and always write a comparison for anything else. Readingif ($user)forces the reader to work out what "a falsy user" would even mean;if ($user !== null)does not.
isset, empty and array_key_exists
These three all answer "is this thing there?", but they answer three different versions of that question, and choosing the wrong one is a steady source of bugs in form handling.
isset() asks: does this variable or key exist and hold something other than null? It returns false for a null value. It never emits a warning about undefined variables, which is precisely what it is for. It also accepts several arguments at once, returning true only if all of them are set.
empty() asks: is this missing, or set to a falsy value? So it returns true for a missing key, for null, for 0, for "", for "0", and for an empty array. That makes it convenient for "did the user actually type something" — and dangerous for numeric fields, for exactly the reason the previous section described. empty($_POST['quantity']) is true when the user typed 0.
array_key_exists() asks the narrowest question: does this key exist in the array, regardless of its value? Unlike isset(), it returns true for a key whose value is null. That distinction matters when null is meaningful data — a database row where middle_name is genuinely NULL exists as a column, and isset($row['middle_name']) would tell you it does not.
A practical rule: use isset() for optional request input, array_key_exists() when null is a real value you need to distinguish from absence, and reach for empty() only for strings where a falsy value cannot be legitimate. In most modern code, ?? replaces isset() entirely — $page = $_GET['page'] ?? 1; is the same check with a default attached.
<?php
$data = [
'name' => 'Ananya',
'nickname' => null,
'age' => 0,
'bio' => '',
];
var_dump(isset($data['name'])); // true
var_dump(isset($data['nickname'])); // false <- exists, but is null
var_dump(isset($data['missing'])); // false
var_dump(empty($data['name'])); // false
var_dump(empty($data['age'])); // TRUE <- 0 is falsy
var_dump(empty($data['bio'])); // true
var_dump(array_key_exists('nickname', $data)); // TRUE - the key is there
var_dump(array_key_exists('missing', $data)); // false
// isset checks a whole chain safely
$order = ['customer' => ['address' => null]];
var_dump(isset($order['customer']['address']['city'])); // false, no warning
// Real form handling
$quantity = $_POST['quantity'] ?? '';
if ($quantity === '') {
$errors[] = 'Quantity is required.';
}
// NOT: if (empty($_POST['quantity'])) - rejects a legitimate 0
// Multiple values at once
if (isset($_POST['email'], $_POST['password'])) {
// both were submitted
} isset()andempty()are language constructs, not functions. That is why they can inspect an undefined variable without triggering a warning — a normal function would have to evaluate its argument first. It also means you cannot use them as callbacks, soarray_filter($arr, 'empty')is a parse error.
switch: Convenient, With Two Sharp Edges
switch compares one value against a list of possibilities. It reads well when you are dispatching on a fixed set of values — a page name, an order status, an action parameter — and it avoids a long ladder of elseif ($action === '...') lines.
The first sharp edge is fall-through. When a case matches, PHP runs everything from there until it hits a break — including the code in the following cases. Forget one break and your "delete" case silently also runs your "archive" case. This behaviour is occasionally useful, which is why it exists: stacking several case labels with no code between them lets them share one block, as in grouping Saturday and Sunday together.
The second edge is that switch compares with loose equality, ==, not ===. PHP 8 removed the worst consequence of this — a non-numeric string no longer equals 0 — but the numeric-string surprises remain, so a case of 1 will match a submitted value of "01". If you are dispatching on user-supplied values, that is a hole.
Both edges are why the next section exists. For new code where you are producing a value, match is simply the better tool. Keep switch for the case where each branch does something substantial rather than returning something.
<?php
$action = $_GET['action'] ?? 'view';
switch ($action) {
case 'edit':
echo "Showing the edit form";
break; // forget this and 'delete' runs too
case 'delete':
echo "Deleting the record";
break;
// Deliberate sharing: both labels, one block
case 'view':
case 'show':
echo "Showing the record";
break;
default:
echo "Unknown action";
}
// The loose-comparison hole
$level = "01";
switch ($level) {
case 1:
echo "matched 1"; // this runs - "01" == 1
break;
case "01":
echo "matched 01"; // never reached
break;
} - Inside a
switchthat sits within a loop,continuebehaves confusingly becauseswitchcounts as a loop structure for that keyword. Usecontinue 2to skip to the next iteration of the enclosing loop, and PHP 8 will warn you if you write a barecontinuethere.
match: What You Should Reach For in PHP 8
match was added in PHP 8 and fixes every complaint about switch at once. It is an expression, so it produces a value you can assign directly. It compares with strict ===. It has no fall-through, so no break is needed. And if nothing matches and you supplied no default, it throws an \UnhandledMatchError rather than silently doing nothing.
That last point is a feature, not an inconvenience. Silent non-matching is how a new order status added six months later ends up displaying a blank label on every invoice, with no error anywhere. A loud exception tells you immediately that a case is missing.
Multiple values can share one arm by separating them with commas, which replaces the stacked-case idiom. Each arm holds a single expression; if you need several statements, call a function from the arm.
The match(true) form deserves its own mention because it is not obvious. Since match compares strictly against the subject, using true as the subject means each arm is simply evaluated as a boolean condition, and the first true one wins. This turns a grading ladder into a compact table where the ranges line up visually — much easier to check for gaps than a stack of elseif blocks.
<?php
$status = 'paid';
// A value, produced directly
$label = match ($status) {
'pending' => 'Awaiting payment',
'paid' => 'Payment received',
'shipped' => 'On the way',
'cancelled', 'refunded' => 'Closed', // several values, one arm
default => 'Unknown',
};
echo $label; // "Payment received"
// No default: a new status becomes a loud error, not a silent blank
$colour = match ($status) {
'pending' => 'orange',
'paid' => 'green',
'shipped' => 'blue',
};
// match(true) for ranges
$marks = 87;
$grade = match (true) {
$marks >= 90 => 'A',
$marks >= 75 => 'B',
$marks >= 60 => 'C',
$marks >= 40 => 'D',
default => 'F',
};
echo $grade; // B
// Strict by design
$input = "1";
$result = match ($input) {
1 => 'integer one',
"1" => 'string one', // this arm matches
default => 'neither',
}; - If you want a missing case to be safe rather than fatal, add
default => .... Choose deliberately: for a fixed internal set such as order statuses, no default is better, because it turns a forgotten case into an error during testing. For values that come from a URL, always supply a default, since a visitor can send anything.
Conditions Inside HTML Templates
In a template file, opening and closing PHP tags around braces produces a mess of stray } characters floating among your HTML. PHP's alternative syntax replaces the closing brace with a keyword that names what it closes: endif;, endforeach;, endwhile;, endfor;, endswitch;.
This makes long templates genuinely readable. When you see <?php endif; ?> forty lines below the condition, you know exactly what it closes without counting braces. Alternative syntax is available for all the control structures and is used throughout WordPress themes and plain-PHP view files, so you will encounter it whether or not you write it yourself.
For printing one of two short values, the ternary operator is neater still — a class name, an selected attribute on a dropdown, a checked state on a checkbox. Just keep them simple: a ternary containing another ternary is unreadable, and in PHP 8 an unbracketed one is a fatal error anyway.
The important discipline for templates remains the one from the first lesson. Decide things in PHP at the top of the file, and let the HTML section contain only simple checks and printing. A template that runs database queries between <div> tags is very hard to change safely later.
<?php
$user = ['name' => 'Ananya', 'role' => 'student'];
$isAdmin = $user['role'] === 'admin';
$subjects = ['Physics', 'Chemistry'];
?>
<nav>
<?php if ($isAdmin): ?>
<a href="/admin">Admin panel</a>
<?php elseif ($user !== null): ?>
<a href="/profile">My profile</a>
<?php else: ?>
<a href="/login">Login</a>
<?php endif; ?>
</nav>
<?php if ($subjects === []): ?>
<p>No subjects registered yet.</p>
<?php else: ?>
<ul>
<?php foreach ($subjects as $s): ?>
<li><?= htmlspecialchars($s) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<!-- Ternary for small decisions -->
<tr class="<?= $isAdmin ? 'row-admin' : 'row-normal' ?>">
<td><?= htmlspecialchars($user['name']) ?></td>
</tr> - Notice
$subjects === []rather thanempty($subjects). Both work here, but the explicit version states the assumption — that$subjectsis an array — which means the code breaks loudly if that assumption ever stops being true, instead of quietly showing the empty message.
