Decisions: if, and the equality trap
Branching with if and elseif, choosing between two values with a ternary, the match expression, and the one comparison rule that saves the most debugging time.
If, elseif, else
A condition is an expression that comes out true or false, and if runs the block after it when it does: if ($score >= 90) { … } elseif ($score >= 80) { … } else { … }. The chain is read top to bottom, and the first true branch wins — order is part of the meaning.
A ternary chooses between two values rather than running blocks: $label = $score >= 50 ? "pass" : "fail";. When the two outcomes are values and the condition is short, a ternary reads better than an if; when either side grows, the if is the kinder form.
Braces are optional for a single statement, and they are still worth writing. Without them, adding a second line to a branch silently moves it outside the if, and an else can attach to the wrong branch. The brace is not decoration; it is the boundary of the block, and it is the first thing a reader looks for when a condition seems to do nothing.
Reading a condition out loud
A condition is easier to trust when you can say it as a sentence. $age >= 18 && $hasTicket reads “old enough and has a ticket”; !$blocked reads “not blocked”. When a condition needs the word “unless” twice, invert it: if (!$blocked && !$expired) is usually clearer written as $active = !$blocked && !$expired; and then if ($active).
The comparison itself should put the thing that varies on one side and the fixed value on the other. if ($status === "paid") is easier to read than if ("paid" === $status), though the reversed form has one advantage: an accidental = becomes a parse error instead of an assignment. Modern editors warn about the assignment either way, so readability usually wins.
Two equals signs, or three
== compares after converting the types to fit, and === compares value and type together. 0 == "0" is true; 0 === "0" is false. "1" == "01" is true, which surprises everyone the first time: both look like the number one, so the conversion says yes.
The rule that prevents most of this: use === for everything, unless you can say precisely why loose comparison is what you want. A function that returns a number or false is the honest exception — there the types genuinely are different answers — and even then, === false is the clearer way to say it.
Match: a chain that returns
match is the modern alternative to a long if chain when every branch produces a value. It compares with the same rules as ===, takes expressions rather than blocks, and — unlike a switch — throws an error when nothing matches and there is no default. A silent fallthrough is impossible, which is the whole point.
The pattern that makes it shine with ranges is match (true): each arm is a condition that is either true or not, and the first true arm wins. It reads like a table, and with strict comparison underneath, it behaves like one.
A grade, and a comparison clinic
<?php
$score = 87;
if ($score >= 90) {
$grade = 'A';
} elseif ($score >= 80) {
$grade = 'B';
} else {
$grade = 'C';
}
echo "Grade: $grade" . PHP_EOL;
// The same shape as a ternary: two values, one condition.
echo ($score >= 80 ? 'pass' : 'fail') . PHP_EOL;
// match (true) reads like a table of conditions.
$label = match (true) {
$score >= 90 => 'excellent',
$score >= 80 => 'good',
default => 'keep going',
};
echo "Label: $label" . PHP_EOL;
// Loose comparison converts; strict comparison does not.
var_dump(0 == '0'); // true
var_dump(0 === '0'); // false
var_dump('1' == '01'); // true — both convert to 1
var_dump('abc' == 0); // false in PHP 8A delivery cost calculator
Run it and read which branch each parcel takes. Then add a same-day option that costs 9.99 regardless of weight, and give the match a new arm for parcels over 30 kg.
An empty string, the string "0", the number 0 and null are all falsy; almost everything else is truthy — including the string "false". Whenever a condition depends on that list, compare explicitly instead of leaning on it.
Truthiness, and the short circuit
An if does not require a boolean; it converts whatever it is given. 0, 0.0, "0", "", null, false and an empty array are false, and almost everything else is true — including the string "false". That list is the reason a condition should compare explicitly when the value could be one of the surprising ones.
&& and || stop as soon as the answer is known. In $user !== null && $user->isActive(), the method is only called when the left side is true, which is what makes the guard safe. match is stricter than a chain of comparisons: it uses ===, every arm is an expression, and a value with no arm and no default throws instead of falling through.
Three decision mistakes are common: writing if ($count = 0) when the comparison was meant, comparing a value that may be 0 or "0" with ==, and leaving the default off a match whose input can be anything. Use ===, put the constant on the left if assignment is a habit, and make the unmatched case explicit.
Try these next
- Write a grade chain with
if,elseifandelse, then test the exact boundaries 80 and 90. - Rewrite the same chain as
match (true)and add adefaultarm. - Print
var_dump()for0 == "0",0 === "0"and"1" == "01", and say which result surprised you.
A shipping desk
Run it for Ireland, then change the country to `DE` and the express flag to `true`. Add a same-day option that costs 12.00 regardless of zone.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Which comparison should be the default in PHP?
