No set of control structures is more pervasive in programming than if
, elseif
and else
. With a few exceptions, you’ll use at least one per function or method that you write. There’s just no way around it.
But conditionals (that’s what we call these control structures) fit in the “easy to learn, hard to master” category. In fact, they’re so easy to use that you can develop some bad habits around them. (This is also a problem with loops.) This can lead to code that’s complex and hard to read or even test.
That said, it’s possible to develop good programming habits with conditionals. This is what this article will try to help you with. We’ll go over some programming techniques that can help make conditionals more manageable.
How does PHP evaluate multiple conditionals?
First, let’s take a look at how PHP evaluates conditionals. This is so often misunderstood when using conditionals. But knowing how PHP evaluates them lets you remove and/or combine conditionals. This, in turn, makes your code simpler.
Evaluation order of a conditional
The first thing that you should always keep in mind is the order that conditionals get evaluated. Most programming languages evaluate conditionals from left to right. PHP is no exception to this.
if (is_array($array) && !empty($var)) { do_something($array, $var); }
Above is a small example to show this in action. In the example’s conditional, PHP will call the is_array
function first. If is_array
returns true
, it’ll then call the empty
function.
Interrupting the evaluation of a conditional
You might have noticed something unusual at the end of the last section. We mentioned that PHP will only call empty
if is_array_returns
true. This is an important property of conditionals in PHP.
PHP will stop evaluating conditionals once it finds a definite result. This means that PHP won’t always run all the code in your conditional statement. An easy way to visualize this is by reworking our earlier example to use nested if
statements.
if (is_array($array)) { if (!empty($var)) { do_something($array, $var); } }
The above nested if
statements are equivalent to our single if
statement from earlier. PHP will start by evaluating the first if
statement. And only if is_array
returns true
will it evaluate the second nested if
statement.
It’s worth noting that both examples are as complex. The number of paths through the code remains unchanged. That said, nested conditionals like the one above tend to make code harder to read. That’s why it’s better to combine them into a single conditional.
A cheat sheet
Below is a set of conditionals that shows all the cases where PHP will exit a conditional early. For all these conditionals, PHP will never call the do_something
function. This is always useful to keep in mind. So feel free to think of this as a cheat sheet when thinking about this property of conditionals.
if (false && do_something()) // ... } elseif (false and do_something()) // ... } elseif (true || do_something()) // ... } elseif (true or do_something()) // ... }
Operator precedence
Let’s imagine that we modified our conditional from earlier and now it looked like this:
if (is_array($array) && !empty($array) || !empty($var)) { do_something($array, $var); }
How does PHP evaluate a logical condition like the one above? Well, it relies on a concept called operator precedence. It tells PHP how to prioritize and group the different operator.
if ((is_array($array) && !empty($array)) || !empty($var)) { do_something($array, $var); }
Above is how PHP sees our logical condition through the lens of operator precedence. It tells PHP to group both the is_array
and empty
check for the array
variable together. And then it combines that group with the second empty
check.
How to manage nested conditionals
Now, we can’t always combine nested conditionals into a single conditional. (Bummer!) But that doesn’t mean we have to use nested conditionals either. We shouldn’t give up just like that!
That’s because nested conditionals are the bad habit that we want to eradicate. In a lot of cases, they’re the main source of complexity in your code. That’s why you should avoid them.
But that’s easier said than done. That said, there are strategies that you can use to avoid them. And that’s what we’ll cover for the rest of this article.
Use guard clauses
The best way to avoid using nested conditionals is by replacing them with guard clauses. The goal of a guard clause is to protect your code so that there aren’t any errors during its execution. You could also view them as gates that you must cross to reach your code.
In practice, guard clauses aren’t any different from the conditionals that you’ve seen so far. In fact, let’s say that we compared the conditionals in a set of guard clauses to a set of nested conditionals. You’d find that they’re almost the same.
The most visible difference between the two is their structure. You don’t want to nest guard clauses. You want your code to pass them one at a time like gates. (That’s why it’s a useful analogy!)
This change in structure isn’t always free. You might need to make changes to the logical condition in each of your conditionals. For example, you might need to invert a condition compared its nested counterpart.
Guard clause examples
The way we’ve described guard clauses so far might not be easy to understand. That’s why we’ll look at two guard clause examples. This should help you understand them better.
Example with a single guard clause
To begin, let’s look at an example with a single conditional:
function do_something_to_array($array) { if (is_array($array)) { $array = clean_array($array); } return $array; }
The conditional in the do_something_to_array
function is pretty straightforward. If is_array
returns true
, we pass array
to the clean_array
function. We then assign the return value back to array
. Once that’s done, the function returns the array
variable.
Now, let’s look at the same function, but using a guard clause:
function do_something_to_array($array) { if (!is_array($array)) { return $array; } return clean_array($array); }
We inverted our conditional so that it can act as a guard clause. To get past it, is_array
must return true. Otherwise, we return the array
value like we did earlier.
Once past the guard clause, we know beyond a doubt that we have an array. This is an important fact that we’ll use later. But for this example, it means that we can just call the clean_array
function and return its return value.
Example with multiple guard clauses
Alright, so this was an example with a single conditional. Let’s increase the difficulty for this next example. Let’s look at what guard clauses look like with nested conditionals!
function do_something_to_array($array) { if (is_array($array)) { $array = clean_array($array); if (count($array) == 1) { return current($array); } } return $array; }
As you can see, this is the same example as earlier except that we added a nested conditional to it. The new conditional checks the size of the array
variable that clean_array
returned. It does it using the count
function.
If array
only has one value in it, we return that value. To do that, we use the current
function. This is a PHP function that returns the current value that the internal array pointer is pointing to.
function do_something_to_array($array) { if (!is_array($array)) { return $array; } $array = clean_array($array); if (count($array) == 1) { return current($array); } return $array }
And here is our reworked guard clause example. It looks a lot more like our example with nested conditionals than it did before. We took everything inside the first conditional and put it in front of our guard clause. Nothing else has changed.
The role of guard clauses
As we mentioned earlier, guard clauses aren’t there to remove conditionals from your code. Instead, they let you restructure your conditionals so that your code is linear. This is what makes it easier to read.
The diagram above illustrates the difference between the two. Nested conditionals are like a pyramid. (You might need to turn your head sideways!) You’re climbing it, but it’s often hard to know what conditions are true from where you are on it.
Meanwhile, each guard clause that PHP goes through is like checking off a prerequisite off a list. You can tell by looking at any part of your code which prerequisites PHP hasn’t checked off yet. This is what makes guard clauses easier to read compared to nested conditionals.
Refactoring nested conditionals to guard clauses
Alright, so you’re convinced! You don’t want nested conditionals in your code anymore. You want guard clauses instead. But how do you convert one to the other? Here’s a diagram that shows you how to do it.
In a lot of cases, you can convert each level of your nested conditionals pyramid into a guard clause. That said, it might not always end up working. But it’s more often than not a good starting point.
The order of the guard clauses follows the order or the pyramid levels. The lowest level of the pyramid is your first guard clause. The second lowest is the second guard clause. You get the idea!
As for the code on each pyramid level, it goes after its level’s guard clause. Sometimes you might need to rework some of the code’s logic. Other times, you’ll be able to copy the code as is. It’s something that you’ll have to figure out on a case-by-case basis.
At this point, you might have noticed that our guard clause examples seem to match this pattern. (Very astute of you!) Those examples do follow this pattern. But, like we mentioned in the previous paragraph, it might not always work out that way.
Return early
“Return early” isn’t a complicated strategy like guard clauses. It just means that you should use return statements early in your function or method if it makes sense to do so.
The goal of this strategy is to reduce the mental clutter in your code. When you return from a function or method early, it’s one less path through your code that you have to keep track of. This leads to code that’s easier to follow and read.
It’s worth noting that both our guard clause examples made use of this strategy. We returned the array
variable if is_array
returned false. We also returned the current value inside array
if it only contained a single value.
In both cases, the path through our code had reached a logical conclusion. We had no use for an array
variable that wasn’t an array or that only contained a single value. So it made sense to use a return statement at both those points in our code.
This brings us to the last point about the “return early” strategy: you can only use it with a guard clause. (Well, you could with a switch
, but that’s it.) But the inverse isn’t true. You don’t have to use the “return early” strategy when you use a guard clause.
Break up nested conditionals
Now, there will be times where none of these strategies will work for you. In those situations, it’s fine for you to use a nested conditional. That said, you should try to avoid keeping them as-is.
Instead, you should break those nested conditionals into their own functions or methods. It’s true that you’re just moving code around. You’re not really removing those nested conditionals by doing this.
But you could say the same about a lot of the strategies that we’ve seen so far. They all share the same goals. They want to increase readability and reduce complexity.
Breaking a nested conditional into functions
As usual, the best way to show you this is with an example. So let’s look at one!
function create_something(array $data) { if (empty($data['type'])) { $data['type'] = 'post'; } if ('page' == $data['type']) { if (empty($data['title'])) { $data['title'] = 'Default page title'; } // Do stuff with page data save($data); } elseif ('post' == $data['type']) { if (empty($data['title'])) { $data['title'] = 'Default post title'; } // Do stuff with post data save($data); } }
We can break down create_something
function above into three parts. First, there’s a guard clause at the beginning. It ensures that our data
array always has a value for the type
key.
You can find the second and third parts inside the if
and elseif
conditional blocks. The if
block contains the code for creating a page. Meanwhile, the elseif
block contains the code for creating a post
. Both blocks have a nested if
statement used to insert a default title if there isn’t one.
The goal here is to show a situation where nested conditionals were a good design decision. That said, even if this was a good design decision, the readability issue remains. It might not seem like it in the example but it’s only because it hides the issue from us.
If those “Do stuff” comments were 20-30 lines of code, it would be a lot harder to read or keep track of what was going on. That’s why we should move these two blocks of code into their own functions.
function create_page(array $page_data) { if (empty($page_data['title'])) { $page_data['title'] = 'Default page title'; } // Do stuff with page data save($page_data); } function create_post(array $post_data) { if (empty($post_data['title'])) { $post_data['title'] = 'Default post title'; } // Do stuff with post data save($post_data); } function create_something(array $data) { if (empty($data['type'])) { $data['type'] = 'post'; } if ('page' == $data['type']) { create_page($data); } elseif ('post' == $data['type']) { create_post($data); } }
As you can see, we created two new functions: create_page
and create_post
. We moved the code from each conditional block into each matching function. This also has the benefit of allowing for code reuse.
But the important result is that the create_something
function is a lot easier to read now. We replaced each conditional block with a single function call. And both function names do a good job of explaining the code that the function replaced.
Leverage the ternary operator
Have you ever seen code using :
and ?
before? In PHP, we use this combination of characters to represent the ternary operator. It’s a lesser known tool in your conditional toolbox.
condition ? value_if_true : value_if_false
In most programming languages, a conditional using the ternary operator follows this format. There’s a condition, a value if the condition is true and a value if the condition is false. The ?
and :
separate each of the three components.
The main goal of the ternary operator is to shorten small conditional statements. These are common when you want to assign a value based on a condition. A common conditional variable assignment scenario is assigning a default value. We had that problem several times in our example breaking conditionals into functions.
function create_something(array $data) { if (empty($data['type'])) { $data['type'] = 'post'; } // ... }
Here’s our create_something
function from earlier. The function begins with a guard clause looking at the type
key inside the data
array. If there’s no value assigned to the type
key, we assign it the default value of post
.
Now, you could also format this guard clause using a ternary operator. It would look like this:
function create_something(array $data) { $data['type'] = empty($data['type']) ? 'post' : $data['type'] // ... }
Let’s look at this conditional statement by analyzing the three ternary operator components. First, we have the empty check which is our condition. Next, there’s the post
string which is our value if the empty check is true.
The last component is a bit different compared to everything that we’ve seen so far. We use $data['type']
as the value if the empty check is false. This is the same as saying, “Keep the same value if the empty check is false.”
Avoid using the “else” statement
Alright! So this final strategy is the culmination of everything that we’ve seen so far. To use it, you’ll have to put in practice every strategy that you’ve seen up to this point.
But how can you not use an else
statement!? This might sound crazy to you. Well, bear with me, there’s a good reason for it!
Why is the “else” statement bad?
The root of the problem with the else
statement is its role. We use it as a catch-all in our conditionals. We do something if the condition is true or else we do something else.
The problem with that is twofold. It makes your code harder to read. And it also tends to make your code more complex.
These two issues shouldn’t surprise you at this point. We’ve been hammering at these two throughout this article. (No reason to not beat them up a bit more!) That said, it’s still worth seeing why these issues are present when you use an else
statement.
Using “else” decreases readability
When we’re programming, we spend a lot more time reading code than we do writing it. That’s why readability is such an important issue. But it isn’t easy to see the impact that an else
statement has on it.
The diagram above attempts to illustrate that impact. For most of us, reading is easier if everything is in a linear path. You go through the code step by step like PHP would when it executes it.
But an else
statement breaks that linear reading flow in your code. You’re stuck having to figure why your code reached the else
statement. This means going back through each if
or elseif
statement to see why each evaluated to false
.
Using “else” increases complexity
The complexity issue is a broader topic. At its root, it has to do with the fact that an else
statement creates a new branch in your conditional. This new branch is more often than not unnecessary. The result is code that’s more complex and harder to maintain.
A complexity example
Let’s look at an example to put this complexity issue in perspective:
function do_something($maybe_array) { if (is_array($maybe_array)) { foreach($maybe_array as $value) { do_something_else($value); } } else { do_something_else($maybe_array); } }
Have you ever written a function with a conditional like the one above? I think that we all have at one point or another in our programming career! Using else
is especially common with the is_array
function.
That said, there’s no reason to use else
in the do_something
function. And, in fact, the use of else
here created a maintainability problem. It forced us to use the do_something_else
function twice in 8 lines of code.
This is bad because it creates duplicate code. This is always something that you should try to avoid. It tends to create code that’s hard to maintain. (That’s why it’s often seen as a code smell)
How to avoid using an “else” statement
So what can we do here? It’s one thing to say to not use else
in your code, but it’s another to do it. It’s not that easy when you’re not used to it.
The trick is to zoom out on your code. You want to look at the big picture and question how this else
statement fits in it. Let’s do it for our do_something
function.
Looking at the conditional in the do_something
function, we can see that it handles two scenarios. In one, maybe_array
is an array. We loop through it passing each value
to do_something_else
. In the other, maybe_array
isn’t an array and we just pass maybe_array
to do_something_else
.
But why do we even need these two scenarios? Why couldn’t we just work under the premise that maybe_array
needs to be an array all the time? That’s the line of questioning that you’re looking for.
The answer here is that we don’t need to support these two scenarios. We can (and should!) say that maybe_array
has to be an array. And that’s how we’ll remove the else
from the do_something
function.
Using a guard clause instead of “else”
By making this design decision, we can fall back on a concept that we saw earlier: the guard clause. If we want maybe_array
to always be an array, we can just add one at the beginning of the do_something
function. This lets us rework the do_something
function into this:
function do_something($maybe_array) { if (!is_array($maybe_array)) { $maybe_array = array($maybe_array); } foreach($maybe_array as $value) { do_something_else($value); } }
As you can see, the else
statement is gone. Instead, we have a guard clause that checks if maybe_array
is an array. If it isn’t, it creates one using the maybe_array
value and assigns it to maybe_array
.
Once passed the guard clause, maybe_array
will always be an array. This means that we can just use the foreach
loop now. We loop through maybe_array
and pass each value
to the do_something_else
function.
One way out of many
So this is how you can remove an else
statement using a guard clause. But you don’t have to limit yourself to using a guard clause either! There’s more than one way to remove an else
statement.
In fact, you can use any of the strategies that you’ve seen in this article. You can even mix strategies together if you want! (The only limit is your imagination!) For example, you might use a guard clause and break up the function into two functions.
That said, the important thing to keep in mind is the perspective described earlier. You should always look at what your code is doing overall. And, using that high-level view, question why is the else
statement needed.
An important concept to master
Wow! So that’s a lot to process for such a mundane (or so you thought!) concept. But mastering the use of conditionals is a great skill to have.
And, like PHP strings, you can apply what you’ve seen in this article in other programming languages. It goes beyond the realm of PHP. It’s fundamental programming knowledge!
Photo Credit: Ruth and Dave