Support the ongoing development of Laravel.io โ†’

Using the "Conditionable" Trait In Laravel

17 Apr, 2024 9 min read

Photo by Tim Krauss on Unsplash

Introduction

Conditions are an integral part of any codebase. Without being able to conditionally execute code (using things such as if and else), our software would be extremely limited in what it could do. In fact, they're pretty much one of the first things you learn when you start programming.

But using if and else can sometimes prevent you from being able to chain method calls together.

In this article, we're going to take a quick look at how to Laravel's Illuminate\Support\Traits\Conditionable trait to help you chain method calls together and add the when method to your own PHP classes in Laravel.

What is the Conditionable Trait?

To get an idea of what the Conditionable trait does, let's look at an example of some code and then rewrite it to use the trait.

How often have you written code like this that conditionally adds a where clause to an Eloquent query?

$query = User::query()
    ->where(...)
    ->where(...);

if (Auth::user()->isAdmin()) {
    $query->where(...);
}

$users = $query->get();

At an initial glance, since this code is split into 3 separate blocks, it might not be immediately obvious whether all the code is related to the query. We might be doing something inside the if block that isn't altering the query but is instead running some extra logic (although we're not doing this example). I would much prefer being able to keep the query in one single chain. But that's just my personal opinion.

Side note: You might have spotted I've used ::query() at the beginning of the query. If you've not come across this before, I have an article that explains what this is and what it's doing: Using 'query()' in Laravel Eloquent Queries.

If it was chained, it might look something more like this:

$users = User::query()
    ->where(...)
    ->where(...)
    ->when(
        Auth::user()->isAdmin(),
        fn ($query) => $query->where(...),
    })
    ->get();

As we can see in the code example above, we've replaced the if block with a when method.

We are able to access this method because the Illuminate\Database\Eloquent\Builder class (which is being used to build up our Eloquent query) uses the Illuminate\Database\Concerns\BuildsQueries trait, which in turn uses the Illuminate\Support\Traits\Conditionable trait.

If the result of the first argument passed to the when method is truthy, then the second argument (a closure or arrow function) is executed. If the result is falsy, then the closure is not executed.

I don't know about you, but I find this much easier to read and understand. My brain is able to instantly look at the code and understand that it's all related to the same thing (in this case, the query). I'm assuming that it's because the code is close together and follows the Gestalt Principle of Proximity?

There are several other places in Laravel's codebase where the when method is available for us to use, such as:

But what about if we want to use it in our own classes?

Let's take a look at how to use the Conditionable trait ourselves.

Adding the Conditionable Trait

It's really easy to start using the Conditionable trait. All you need to do is add it to your class.

Let's imagine we have this example class that can be used for building and storing a PDF report:

declare(strict_types=1);

namespace App\Services;

final class ReportBuilder
{
    private bool $includeCharts = false;

    private bool $includeTables = false;

    private string $colour;

    public function buildPdfReport(): string
    {
        // Build the PDF report.
        // Save it in storage (e.g. - S3).
        // Return the URL of the PDF report for downloading.
    }

    public function includeCharts(): self
    {
        $this->includeCharts = true;

        return $this;
    }

    public function includeTables(): self
    {
        $this->includeTables = true;

        return $this;
    }

    public function darkMode(): self
    {
        $this->colour = 'dark';

        return $this;
    }

    public function lightMode(): self
    {
        $this->colour = 'light';

        return $this;
    }
}

We might want to use this code inside one of our controllers like so:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\ReportBuilder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ReportController extends Controller
{
    public function __invoke(Request $request, ReportBuilder $reportBuilder): JsonResponse
    {
        if ($request->boolean('includeCharts')) {
            $reportBuilder->includeCharts();
        }

        if ($request->boolean('includeTables')) {
            $reportBuilder->includeTables();
        }

        $request->boolean('darkMode')
            ? $reportBuilder->darkMode()
            : $reportBuilder->lightMode();

        $url = $reportBuilder->buildPdfReport();

        return response()->json([
            'url' => $url,
        ]);
    }
}

In the example above, we're using if and else statements to conditionally call methods on the ReportBuilder class. After we've built the PDF report, we return the URL of the PDF report in a JSON response.

Let's update our ReportBuilder class to use the Conditionable trait:

declare(strict_types=1);

namespace App\Services;

use Illuminate\Support\Traits\Conditionable;

final class ReportBuilder
{
    use Conditionable;
    
    // The rest of the class...
}

From here, we can then refactor our controller to use the when method:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\ReportBuilder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ReportController extends Controller
{
    public function __invoke(Request $request, ReportBuilder $reportBuilder): JsonResponse
    {
        $url = $reportBuilder
            ->when(
                value: $request->boolean('includeCharts'),
                callback: fn ($builder) => $builder->includeCharts(),
            )->when(
                value: $request->boolean('includeTables'),
                callback: fn ($builder) => $builder->includeTables(),
            )->when(
                value: $request->boolean('darkMode'),
                callback: fn ($builder) => $builder->darkMode(),
                default: fn ($builder) => $builder->lightMode(),
            )->buildPdfReport();

        return response()->json([
            'url' => $url,
        ]);
    }
}

As we can see in the code example above, we've replaced the if and else statements with the when method.

You might have noticed that we've also passed a default argument to the third when method that determines what should happen if the value is falsy. If the value is falsy, then the lightMode method will be called instead.

Note: For the purposes of keeping the code examples short and understandable, I've not added return types and type hints to the arrow functions. In a real-world application, I'd recommend adding these for extra type safety and clarity.

Passing Values to the when Callback

Although we've not covered it in the examples above, you can access the result of the first parameter (the value parameter) in the callback function (the callback parameter). This can be quite handy if you need to use the value in the callback function.

For example, let's take this block of code:

Post::query()
    ->when(
        value: Auth::user(),
        callback: function (Builder $query, User $user) {
            // We can access the $user variable here...
            $query->where('user_id', $user->id);
        },
    )

In the example above, if there is a logged-in user (accessed using Auth::user()), then it will be passed as the second argument to the callback function.

The unless Method

In addition to the when method, the Conditionable trait also provides an unless method. This method is the opposite of the when method. If the value is falsy, then the callback function is executed. If the value is truthy, then the callback function is not executed.

For example, we might want to write a query to fetch some blog posts that the user can see. If the user is an admin, then we want to fetch all the posts. If the user is not an admin, then we only want to fetch the published posts:

Post::query()
    ->unless(
        value: Auth::user()->isAdmin(),
        callback: function (Builder $query) {
            $query->where('status', 'published');
        },
    )

Personally, I find the unless method quite difficult to read and understand. It takes me a while to wrap my head around the logic that's being used, so I much prefer to use the when method instead.

But that's not a critique of the unless method. It's just due to the way my brain works. You might be the opposite to me and find the unless method easier to understand and use.

Should I Use the Conditionable Trait?

As with a lot of things in development, it's all down to personal preference whether you want to use the Conditionable trait or not.

For me, I find it much easier to read and understand code that uses the when method when I'm chaining method calls together. It keeps everything related to the same thing in one place.

However, there are definitely times when the if and else statements are more appropriate. For example, if you have a lot of logic that needs to be executed, then it might be better to use an if statement. You don't want to have a huge closure that's difficult to read and understand. Or, it might just be that you find if and else statements easier to read and understand.

So it's definitely something you should consider on a case-by-case basis. There's no right or wrong answer here. Just make sure it's something yourself and your team are comfortable using and understand.

If you choose to refactor any of your code from using if and else statements to using the when method, then I'd recommend making sure you have some tests in place that cover the refactored code. This will help you to ensure that the refactored code still works as expected and that you don't break anything in the process.

Conclusion

Hopefully, this article has given you a quick insight into what the Conditionable trait is and how you can use it in your own codebase.

If you enjoyed reading this post, I'd love to hear about it. Likewise, if you have any feedback to improve the future ones, I'd also love to hear that too.

You might also be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.

Or, you might want to check out my other 440+ ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.

If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter below.

Keep on building awesome stuff! ๐Ÿš€lara

Last updated 2 weeks ago.

driesvints, itszun liked this article

2
Like this article? Let the author know and give them a clap!
ash-jc-allen (Ash Allen) I'm a freelance Laravel web developer from Preston, UK. I maintain the Ash Allen Design blog and get to work on loads of cool and exciting projects ๐Ÿš€

Other articles you might like

November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello ๐Ÿ‘‹ Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
November 11th 2024

๐Ÿฃ Sushi โ€” Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.