Back to articles

6 ways to avoid IF statements in Laravel

July 14th, 2021

If statements are evil and should be avoided at all costs. Just kidding – but reducing the number of them can increase the readability/size of your code.

Here are 6 ways to lean on Laravel functionality that tuck IF statements away behind the scenes so you don’t have to write them.

throw_if/throw_unless

If you need to throw an exception based on a condition, chances are you'd write something like this.

if ($user->inactive()) {
    throw new InactiveUserException();
}

Laravel gives us two helper functions, throw_if and throw_unless, that can replicate the IF statement above in one quick call.

throw_if($user->inactive(), new InactiveUserException());

If it suits your condition better, lean on throw_unless.

throw_unless($user->active(), new InactiveUserException());

optional

The PHP 8 nullsafe operator removes the need for this helper, but if you're working with an older PHP version, the optional helper replicates this.

Let's say you needed to access a user's country name, knowing that the country relation might not exist.

if ($user->country) {
    $country = $user->country->name;
}

Absolutely fine, but with optional we can clean this up nicely.

$country = optional($user->country)->name;

The optional helper works by returning a class that allows empty properties to be accessed with the __get magic method. This returns null if the actual property you want isn't available.

Things get a little messy when wrapping multiple objects, for example – if the user could be unavailable too.

$country = optional(optional($user)->country)->name;

If you're using PHP 8, the nullsafe operator makes this a lot cleaner.

$country = $user?->country?->name;

firstOrCreate, etc

If you needed to insert a record into the database only if it doesn't already exist, you might do something like this.

if (!Visit::whereIp(request()->ip())->count()) {
    Visit::create([
        'ip' => request()->ip()
    ]);
}

In this example, we're making sure a visit can't be logged twice if a record with the same IP address already exists.

A better way to do this is using the firstOrCreate method.

$visit = Visit::firstOrCreate(['ip' => request()->ip()], [
    'ip' => request()->ip()
]);

The first array takes the unique columns you want to check with, and the second array is the data you want to insert.

$visit gets assigned either the existing record, or the newly created one. You can use the wasRecentlyCreated property on the model you get back if you need to perform another action based on whether it was created or not.

Other methods like this exist too (firstOrNew, updateOrCreate, findOrNew, etc).

when, unless, whenEmpty and whenNotEmpty

Both Laravel's Model and Collection classes contain methods to perform actions within a closure based on a condition. These implementations are slightly different between Model (which uses the Conditionable trait) and Collection (which uses the EnumeratesValues trait).

On models

Let's look at an example of deleting a user if their account is scheduled to be deleted.

if ($user->scheduledForDeletion()) {
    $user->delete();
}

Not bad, but let's use the when method.

$user->when($user->scheduledForDeletion(), function ($user) {
    $user->delete();
});

I know what you're thinking... that's more code! You're absolutely right, but for smaller actions like this, using PHP's arrow functions reduces this nicely.

$user->when($user->scheduledForDeletion(), fn ($user) => $user->delete());

There's also an unless method if it suits your condition better.

On Collections

Collections use the EnumeratesValues trait which contains when, whenEmpty and whenNotEmpty methods.

Focusing on the when method, it works very similarly to models.

if ($users->count() === 100) {
    $newlyRegisteredUser->awardPrize();
}

If $users is a collection of users, we can use when to get rid of the IF statement.

$users->when($users->count() === 100, function ($users) use ($newlyRegisteredUser) {
    $newlyRegisteredUser->awardPrize();
});

Once again, it could be argued that for a simple condition and action, this is too much. Even shortening it with an arrow function doesn't provide much clarity.

$users->when($users->count() === 100, fn ($users) => $newlyRegisteredUser->awardPrize());

Either way, with the right example, these could read much better than an IF statement. Try them out for your use-case and you might be surprised.

The whenEmpty and whenNotEmpty methods are similar, and you can read more about them in the Laravel documentation.

abort_if/abort_unless

If you're aborting requests manually, rather than relying on Laravel doing it for you, you may write something like this.

if ($user->inactive()) {
    abort(403, 'Unauthorized', []);
}

In comes abort_if to reduce this to one line.

abort_if($user->inactive(), 403);

You know what's coming... there's an abort_unless function too.

abort_unless($user->active(), 403);

firstOrFail

Route model binding takes care of throwing a 404 if a model isn't found, but if you're relying on looking up a model manually (perhaps through form/query data), then you might do this.

if (!$plan = Plan::whereStripeId($request->stripe_id)->first()) {
    throw new NotFoundHttpException();
}

In this example, we're looking up a plan by its Stripe ID, assigning it within the IF statement and throwing a NotFoundHttpException which renders a 404 page.

Instead, you can use firstOrFail to do all this for you.

$plan = Plan::whereStripeId($request->stripe_id)->firstOrFail();

There are probably more

I don't doubt there are a load more ways to reduce the number of IF statements you write in Laravel. If you find any, let me know and I'll add them here.

Happy IF-removing!

Author
Alex Garrett-Smith

Comments

No coments, yet. Be the first to leave a comment.