Mastering the use of PHP conditionals

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.

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.

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.

Operator precedence

Let’s imagine that we modified our conditional from earlier and now it looked like this:

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.

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.

Guard clause diagram

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:

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:

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!

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.

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.

Nested conditionals vs Guard clauses diagram

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.

nested-to-guard-diagram

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!

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.

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.

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:

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.

if/elseif/else readability diagram

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:

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:

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

  • Patrick Zeinert

    The nested conditionals vs. guard clauses graphic was the pivotal moment in me grokking cyclomatic complexity and how I might avoid it. Thanks so much for the regular updates here.

    As a side note, you appear to have some “duplicate code” with the last two paragraphs under the *One way out of many* sub-heading.

    • Fixed. Thanks for spotting that. It’s tough to not tunnel vision after spending so much time on this!

      Glad you liked the diagram! I struggled to find a good way to present it. I plan on doing a primer/introduction on code complexity soon. I mention it too often to not have something written down. lol

  • Justin Tucker

    Excellent article! Ever since I learned about guard clauses, I cringe when I see verbos if/else logic. It takes a little more time to think it through, but the end effect is also easier for maintainability.

    • Oh yeah! It’s one of the most important coding habit that I have. I never use else. But everything in this article, I consider really good habits to have.