The Ternary Operator

Mon 8 Jun 2020

I have never been a fan of the ternary operator. It is often misused and cluttering code with unreadable nested statements. I'm fine with one-time, easy to read use of the ternary operator. However, I dislike it when it becomes part of an important functional evaluation, making methods unreadable. Here I favor longer more explicit statements that often are much easier to read.

Then there are the elvis or shorthand and null coalescing operator expanding on the behavior of the ternary operator. On its own, simple in use, and easy to read. When nested however it will make code harder to understand mashes statements together into a dense unreadable and unmaintainable code.

Since PHP7 developers are warned against nested ternary without parentheses because of the strange left-associative behavior that many developers feel conflicted about. In PHP8 this will be converted to a compile error discouraging the use of these even more. As such, when this becomes obsolete, this will solve many of the behavioral quirks that are now possible.

I will be showing some interesting bits of code that show some of the quirks of the ternary operator.

Basic and nested ternary operator

The basic ternary operator is simple enough. I will provide a few examples I've borrowed from David Walsh's blog as he already came up with some good examples. Also borrowed an example of the nested ternary operator from stitcher.io's article.

/* a) If the user is logged in, show him/her name else guest */
$message = 'Hello '.($user->is_logged_in() ? $user->get('first_name') : 'Guest');

/* b) Shorthand evaluation (Elvis) usage */
$message = 'Hello '.($user->get('first_name') ?: 'Guest');

/* 
 * c) Not a fan of the following. Imho evaluation logic should always become before output
 * echo, inline 
 */
echo 'Based on your score, you are a ',($score > 10 ? 'genius' : 'nobody'); //harsh!

/* d)  a bit tougher with a nested ternary */
$score = 10;
$age = 20;
echo 'Taking into account your age and score, you are: ',($age > 10 ? ($score < 80 ? 'behind' : 'above average') : ($score < 50 ? 'behind' : 'above average')); // returns 'You are behind' 

/* e) "thankfully-you-don't-need-to-maintain-this" (unreadabe code) level */
 $days = ($month == 2 ? ($year % 4 ? 28 : ($year % 100 ? 29 : ($year %400 ? 28 : 29))) : (($month - 1) % 7 % 2 ? 30 : 31)); //returns days in the given month

/* f) Nested ternary operator and the left-associative behaviour that comes with it */
$result = $firstCondition
    ? 'truth'
    : $elseCondition
        ? 'elseTrue'
        : 'elseFalse';

I've seen all these examples in the wild in some shape, way, or form. example a) and example b) is the most acceptable to me. I'm not a fan of concatenating string like that, but hey, it works. There are a few things to note doing things this way. In example a we can fully control the condition logic, in b we do not. In example b the user's first name gets evaluated. This is done by a loose type check, here is a good reference of how these are done.

Then from here on things get icky. Example c on its own is acceptable. However, at this point functional logic and output are being combined. This is often the stepping stone to spaghetti code. Most frameworks will guide you and try to refrain from doing these sorts of things. In most frameworks view logic is completely separate and developers will need to provide the views with all the right values upfront preventing you from needing this.

Example d. Do you need to evaluate such logic in an echo statement? It already becomes harder to read and maintain. This sort of logic gets flagged for refactoring or thrown in the backlog as a technical debt or I will do it myself when there is time to do such a thing. Then there is example e. Honestly, I will not even try to understand these kinds of statements. At this point I surrender. If I encounter these I will, depending on how new the code is, blame the code and give it back for refactoring.

The point is that example d and e are feeding bad habits. Let's say you need to change a small bit of logic in either one of those. You start by understanding these statements, this will take you a while. After you've got a good understanding it will be easier to mash your new code into the spaghetti rather than doing the right thing, pulling this apart and giving it a better home in the codebase.

Lastly we have example f. PHP is, unlike other languages, left-associative. Meaning the code is parsed differently. In Stitcher's article you can find this link with an excellent example explaining how PHP parses code in these situations.

/* OP's code */
$a = 2; 
echo ( 
    $a == 1 ? 'one' : 
    $a == 2 ? 'two' : 
    $a == 3 ? 'three' : 
    $a == 4 ? 'four' : 'other'); 
echo "\n";   # prints 'four'

Then if you look at the example provided it will start to make sense. Pay close attention to the parentheses around the statements.

/* How you would expect the OP's code to work like */
$a = 2;
echo ($a == 1 ? 'one' :
     ($a == 2 ? 'two' :
     ($a == 3 ? 'three' :
     ($a == 4 ? 'four' : 'other'))));    # prints 'two'

/* How you would it actually behaves */
echo (((($a == 1  ? 'one' :
         $a == 2) ? 'two' :
         $a == 3) ? 'three' :
         $a == 4) ? 'four' : 'other');   # prints 'four'

You can "solve" this by adding parentheses like the given examples that explain the behavior. However, by doing so, as proven by the other answers and comments, you've created a situation that many developers feel different about. If that is the case in any coding situation then I feel it should be avoided all together.

PHP8 will "solve" this strange behavior by making nested ternary statements without parentheses throw compile errors.

The "elvis" shorthand operator

The shorthand operator or otherwise known as the elvis operator allows you to write quick, short evaluation statements in one line. When the left operand is truthy it will assign the variable with that value.

/* This will try and evaluate $initial, if true then $result = $initial, else 'default' */
$result = $initial ?: 'default';

/* A few examples to show loosy checks */
var_dump(5 ?: 0); // 5
var_dump(false ?: 0); // 0
var_dump(null ?: 'foo'); // 'foo'
var_dump(true ?: 123); // true
var_dump('rock' ?: 'roll'); // 'rock'

As with the other example these are all loosy checks. They are fine when not abusing the ternary operator and keeping it simple. When overused it will become a tangled mess of loosy checks that are nasty to debug when you don't know whats happening.

Null coalescing operator

Then there is the null coalescing operator. It is very similar to the elvis operator. A handy feature with the null coalescing operator is that you can chain it. These examples have been borrowed from tutorialspoint.com.

/* Normal ternary variant */
$username = isset($_GET['username']) ? $_GET['username'] : 'not passed';

/* Null coalescing operator */
$username = $_GET['username'] ?? 'not passed';

/* Chaining ?? operation */
$username = $_GET['username'] ?? $_POST['username'] ?? 'not passed';

Once again, using these on their own are okay. Nesting these can give some headaches.

TL;DR

I almost always advise against the use of the ternary operator. There are too many quirks that make it easy to "spaghetti" code. Making it harder to understand in the process. Performance wise code will perform virtually the same. Then next to performance, readability of code is the one of the most important aspects when working together with other developers on the same project. When this becomes important, I advise against the use of these. It will prevent code to become unmanageable and unreadable.