Laravel: Conditionable is very powerful
Just figured out there is a lot of power on a simple call
Some years ago, Laravel started to include the Conditionable trait into their main classes, like the Eloquent Builder and the Fluent class, among others.
It’s sugar-syntax to avoid using an if/else block, with the idea of making objects method be called fluently without making your code like something a toddler would write with a hammer during an a turbulence.
I’ve seen some people not using it at its full power, hence why this article exists. Let’s start with the most basic and end with the more advanced way to use the trait.
Simple condition
The when() and unless() methods accepts a condition as first parameter, either a truthy or falsy value, or a callback that evaluates into truthy or falsy. The second parameter receives a callable that will be executed if the condition is true. As you can guess, unless() works the opposite for the value. Both callbacks receive the object instance as argument.
// Before
$route = DeliveryRoute::forCart($cart);
if ($route->isOutOfBounds()) {
$route->adjustToClosestDestination();
};
$route->save();
// After
DeliveryRoute::forCart($cart)
->when(
fn ($route) => $route->isOutOfBounds(),
fn ($route) => $route->adjustToClosestDestination(),
)
->save();
// Or...
DeliveryRoute::forCart($cart)
->unless(
fn ($route) => $route->isInsideBounds(),
fn ($route) => $route->adjustToClosestDestination(),
)
->save();If the condition is not true, a default third parameter as a callback can be given to be executed in that case.
// Before
$route = DeliveryRoute::forCart($cart);
if ($route->isOutOfBounds()) {
$route->adjustToClosestDestination();
};
$route->save();
// After
DeliveryRoute::forCart($cart)
->when(
fn ($route) => $route->isOutOfBounds(),
fn ($route) => $route->adjustToClosestDestination(),
fn ($route) => $route->limitEditToBounds(),
)
->save();In both cases, both when() and unless() return the same object instance. If you want to override what these methods return, you can make each callback return something else (as long it’s not null).
// Before
$route = DeliveryRoute::forCart($cart);
if ($route->isOutOfBounds()) {
$route->adjustToClosestDestination();
} else {
// Create a dummy route that is never persisted in the db.
$route = DeliveryRoute::undeliverable();
}
$ticket->save();
// After
DeliveryRoute::forCart($cart)
->when(
fn ($route) => $route->isOutOfBounds(),
fn ($route) => $route->adjustToClosestDestination(),
// Create a dummy route that is never persisted in the db.
fn () => DeliveryRoute::undeliverable()
)
->save();Proxy a method
The second, and probably most useful way to use when() and unless() is by simply using a value to be evaluated. When no second parameter is given, the next method call will be executed as long the condition is true, which saves having to write the callback.
// Before
$route = DeliveryRoute::forCart($cart);
if ($route->isOutOfBounds()) {
$route->adjustToClosestDestination();
}
$route->save();
// After
DeliveryRoute::forCart($cart)
->when(fn ($cart) => $cart->isOutOfBounds())->adjustToClosestDestination()
->save();If the condition is not true, the next method is never executed, skipping it from the fluent call.
Proxy a condition
The third way is to just proxy a method that returns a condition to be evaluated. This is great if, for example, you want to execute a method without having to break your code using an if/else block.
// Before
$route = DeliveryRoute::forCart($cart);
if ($route->isOutOfBounds()) {
$route->adjustToClosestDestination();
}
$route->save();
// After
DeliveryRoute::forCart($cart)
->when()->isOutOfBounds()->adjustToClosestDestination()
->save();⚠ Thread carefully ⚠
Especial care must be taken when proxying a method by using when() and unless() with one or no arguments.
If the method to be proxied returns anything else than the same object, like a scalar or another class instance, that will be the end result. In that case, you should use a callback as second parameter instead.
Let’s imagine the useNoCashPayments() returns void. If we proxy that call, the next method will result in an error since we’re calling a member from a null value instead of the object.
// Fails
DeliveryRoute::forCart($cart)
->when(fn ($cart) => $cart->isOutOfBounds())
->useNoCashPayments()
->save();
// Fails
DeliveryRoute::forCart($cart)
->when()->isOutOfBounds()->useNoCashPayments()
->save()
// Succeeds
DeliveryRoute::forCart($cart)
->when(
fn ($cart) => $cart->isOutOfBounds(),
fn ($cart) => $cart->useNoCashPayments(),
)
->save();