PHP Ternary Operator: Nested Ternaries, ?:, ?? and match

Quick Summary
- Write condition ? value_if_true : value_if_false to pick one of two values; since PHP 8 a nested ternary needs parentheses around the inner one, or the file stops with a fatal error.
- $a ?: $b falls back on any falsy value, while $a ?? $b falls back only when $a is missing or null and gives no warning; $a ??= $b assigns a default the same way.
- For three or more outcomes, use a PHP 8 match expression: it compares with ===, returns a value and throws UnhandledMatchError when nothing matches and there is no default.
The ternary operator is a short way to write an if/else that picks a value: condition ? value_if_true : value_if_false. You can nest a second ternary inside the first, but since PHP 8 the inner one must be wrapped in parentheses, or the whole file stops with a fatal error. For “this value or a default”, PHP has shorter operators: ?:, ?? and ??=. With three or more outcomes, a match expression (PHP 8.0 and later) is easier to read than a nested ternary. The outputs shown below are from PHP 8.2 and 8.4.
How does the ternary operator work?
This snippet shows the same check three ways: a normal if/else if/else chain, a simple ternary and a nested ternary.
//normal
$n = 12;
$ns = 12;
if( 12 == $n ){
echo "Twelve";
}else if( 10 == $n ){
echo "Ten";
} else {
echo "Just a number";
}
echo "\n";
$numberInWord = (12 == $n ) ? "Twelve" : "Just a number";
$numberInWordNested = ( 12 == $ns ) ? "Twelve" : ( ( 10 == $ns ) ? "Ten" : "Just a number" );
echo $numberInWord;
echo "\n";
echo $numberInWordNested;It prints Twelve three times, one per line.
- The
ifblock printsTwelvebecause$nis 12. It also has a branch for 10 and a fallback. $numberInWorduses a single ternary: if12 == $nit getsTwelve, otherwiseJust a number. Unlike the if block, it has no check for 10.$numberInWordNestedadds the check for 10 by putting a second ternary, wrapped in parentheses, in the false branch of the first.- Writing the number first (
12 == $n, a Yoda condition) means a typo like12 = $ncauses an error instead of silently assigning a value.
How do you nest ternary operators safely?
Before PHP 8, the ternary operator was left-associative, so without the inner parentheses the nested line was read as ( ( 12 == $ns ) ? "Twelve" : ( 10 == $ns ) ) ? "Ten" : "Just a number". On PHP 7 that returns Ten when $ns is 12. PHP 7.4 deprecated this form, and PHP 8 doesn’t run it at all: the file stops when it’s compiled, before its first line runs, with this message:
Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported. Use either `(a ? b : c) ? d : e` or `a ? b : (c ? d : e)`That’s why the snippet wraps the inner ternary in parentheses: ( 12 == $ns ) ? "Twelve" : ( ( 10 == $ns ) ? "Ten" : "Just a number" ). The same rule applies when you mix a full ternary with the short form: $a ?: $b ? $c : $d stops with the same kind of error. One nested level is still readable; beyond that, use match, shown below.
What does the short ternary ?: do?
$a ?: $b returns $a if it’s truthy and $b otherwise. It’s short for $a ? $a : $b, except that $a is evaluated only once.
$title = '';
echo $title ?: 'Untitled'; // Untitled
echo "\n";
$qty = 0;
echo $qty ?: 'none'; // none: 0 is falsy too
echo "\n";
$a = 0;
$b = '';
$c = 'third';
echo $a ?: $b ?: $c; // third: the first truthy valueEvery falsy value falls through to the default: '', '0', 0, 0.0, null, false and an empty array. You can chain several ?: without parentheses. Unlike ??, it still warns when the variable or array key doesn’t exist: when the URL has no sort parameter, $_GET['sort'] ?: 'date' returns date but also prints the warning Undefined array key "sort".
What does the null coalescing operator ?? do?
$a ?? $b returns $a if it exists and isn’t null, and $b otherwise. It checks the left side like isset() does, so a missing variable or array key gives no warning. That makes it the usual way to read a request value with a default (PHP 7.0 and later):
$query = [ 'page' => '0' ]; // for example $_GET for the URL ?page=0
$page = $query['page'] ?? 1;
var_dump( $page ); // string(1) "0": the key exists, so its value is kept
$sort = $query['sort'] ?? 'date';
var_dump( $sort ); // string(4) "date": the key is missing, and there's no warning
var_dump( $query['page'] ?: 1 ); // int(1): ?: treats "0" as false
$settings = [ 'color' => null ];
var_dump( $settings['color'] ?? 'blue' ); // string(4) "blue": null counts as missing
var_dump( $settings['size']['width'] ?? 'auto' ); // string(4) "auto": nested keys work too
echo $user_name ?? 'Guest'; // Guest: an undefined variable, no warningUse ?? when only a missing or null value should get the default, and ?: when empty strings and zeros should get it too.
How does ??= work?
$a ??= $b (PHP 7.4 and later) assigns $b only when $a is missing or null, which makes it a short way to fill in defaults:
$options = [ 'color' => 'red' ];
$options['color'] ??= 'blue'; // already set: stays red
$options['size'] ??= 'medium'; // missing: becomes medium
print_r( $options ); // [color] => red, [size] => mediummatch: the PHP 8 alternative to nested ternaries
A match expression compares one value with a list of arms and returns the result of the first arm that matches. Here is the nested ternary from the first example written with it:
$ns = 12;
$numberInWord = match ( $ns ) {
12 => 'Twelve',
10 => 'Ten',
default => 'Just a number',
};
echo $numberInWord; // TwelveHow match differs from a switch or a chain of ternaries:
- It returns a value, so you assign it directly, and there’s no
breakto forget. - It compares with
===: the string'12'doesn’t match the arm12. Aswitchcompares with==, where'12' == 12is true. - One arm can list several values:
'sat', 'sun' => 'Weekend'. - If no arm matches and there’s no
default, PHP throws anUnhandledMatchError(“Unhandled match case 7”) instead of quietly returning nothing.
For ranges, match against true: the first arm whose condition is true wins.
$score = 72;
$grade = match ( true ) {
$score >= 80 => 'A',
$score >= 60 => 'B',
$score >= 40 => 'C',
default => 'F',
};
echo $grade; // BUse a ternary for two outcomes, match when one value decides between several results, and if/elseif or a switch statement when each branch has to run several statements rather than return a value.
Why does my ternary return the wrong value?
Usually it’s operator precedence. The ternary operator and ?? have lower precedence than the . that joins strings, so PHP joins first and tests the result:
$is_member = false;
echo 'Status: ' . $is_member ? 'member' : 'guest'; // member
echo "\n";
echo 'Status: ' . ( $is_member ? 'member' : 'guest' ); // Status: guest
echo "\n";
$name = null;
echo 'Hello ' . $name ?? 'Guest'; // Hello
echo "\n";
echo 'Hello ' . ( $name ?? 'Guest' ); // Hello GuestThe first line tests 'Status: ' . $is_member, a non-empty string, so the condition is always true and only member is printed. The third line hands 'Hello ' to ??, which isn’t null, so the default is never used. Wrap a ternary or ?? in parentheses whenever it’s part of a longer expression.
Related articles
- PHP operator precedence: why parentheses also change the result of || and or
- switch case in PHP: the statement form, for branches that run code instead of returning a value

