Using the "Conditionable" Trait In Laravel
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:
-
String helpers (Example:
Str::of('Ash')->when(...)
) -
Collections (Examlpe:
collect([...])->when(...)
)
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
driesvints, itszun liked this article