optional() is now a helper function built into the Laravel 5.5 core.
A recent Laravel 5.4 addition inspired me to discover an expressive approach to handling PHP objects that may be null
. When higher-order tap()
appeared in Laravel’s commit history last week (see Taylor Otwell’s explanation), I immediately connected it to transform() that I proposed for conditionally changing primitive values.
I want to eliminate these goofy conditionals, fluff greyed out:
- if (Auth::check() && Auth::user()->phone) {
- {{ old(‘address_uuid’, $company->primaryAddress ?
$company->primaryAddress->uuid : null) }} - return $this->latestDraft && $this->latestDraft->isPending();
The goals I have in mind are:
- Reduce repetition of variables, property names, and class methods that make conditionals more resistant to change. For requirement adjustments, think of it as avoiding a programmer’s death by 1,000 cuts.
- Allow expression of requirements in left-to-right statements of natural English, eliminating bleep-blorp speak.
So using a higher-order wrapper class, I came up with global helper function optional()
. It’s a fluid and more succinct approach of calling an object method or accessing its properties when that instance may be null
.
Here are some common examples in Laravel applications.
Formatting a nullable Eloquent date
Working with notifiable classes, the read_at
attribute’s date mutator will give a Carbon
object when a date is present in the database column. To show the duration since someone read their notification the Blade snippet may look like:
@if ($notification->read())
{{ $notification->read_at->diffForHumans() }}
@endif
To make that view more concise and allow reuse of the conditional in other views, it can be implemented as an App\Models\Notification
decorator method using model presenters (implemented using laracasts/presenter or hemp/presenter):
public class NotificationPresenter extends Presenter
{
public function whenRead()
{
if ($this->read()) {
return $this->read_at->diffForHumans();
}
}
}
// {{ $notification->present()->whenRead }}
Well that overwrought solution just made me type “read” four times. Let’s just tell the view what we want in plain language:
{{ optional($notification->read_at)->diffForHumans() }}
old()
default values from relationships
Many form layouts can share the responsibility of both create and edit actions for the given resource. When setting form input defaults, how do you handle values coming from relationships that may have not been be stored yet?
Here are three possible approaches to pre-filling a payment form’s billing address based on the logged in user’s history.
Ternary operator in the darkest timeline
<input id="address1"
name="address1"
value="{{ old('address1', Auth::user()->billingAddress
? Auth::user()->billingAddress->address1
: null
) }}">
If you’re on mobile, that ugly last line probably fell off your screen. These type of expressions are why I came up optional
.
Default to a blank model, passed controller to view
return view('billing.payment')
->with('billingAddress', Auth::user()->billingAddress ?? new Address);
Then the view becomes:
<input id="address1"
name="address1"
value="{{ old('address1', $address->address1) }}">
That view does read much nicer but now there’s a dependency to the controller. I prefer to pass as few variables as possible into view()
to reduce cognitive overhead. And same as the notification timestamp example – I’m typing “address” how many times?
optional()
simplifies it to a one-liner
Marvel at its beauty:
<input id="address1"
name="address1"
value="{{ old('address1', optional(Auth::user()->billingAddress)->address1) }}">
Saving Eloquent model for nullable foreign keys
Another use case is setting relationships when storing an Eloquent model. Laravel gives a nice fluent interface for defining one of the table relationships, but when that related model is related to other tables, ternary operators are usually the approach du jour.
Here is a processed order being marked as paid completely, either as a self-checkout through the online payment gateway ($payment
exists) or an offline payment being OK’d by finance ($payment
is null
.) The reconciliation history entry can look like:
// Order@complete() for $order->complete($payment)
public function complete(Payment $payment = null)
{
return $this->transactions()->create([
'payment_id' => optional($payment)->id,
'status' => OrderStatus::PaymentComplete,
// etc.
]);
}
Model predicate method on a nullable relationship
HasOne
and BelongsTo
relationships resolving to a single Eloquent model instance are ideal for a terse optional
.
public class Article extends Model
{
// ...
public function latestReviewRequest()
{
return $this->hasOne(ReviewRequest::class)
->orderBy('created_at', 'desc');
}
public function isPendingReview()
{
// If you want to get nitpicky, prefix this with !! to force a bool return type.
// as a null `$this->latestReviewRequest` will cause falsy `null` to be returned.
return optional($this->latestReviewRequest)->isPending();
}
}
Also keep in mind Laravel 5.4 added default models for the HasOne
relation so you can try a null object instead of even using optional()
.
public class Article extends Model
{
public function latestReviewRequest()
{
return $this->hasOne(ReviewRequest::class)
->orderBy('created_at', 'desc')
->useDefault(['status' => ReviewStatus::Pending]);
}
public function isPendingReview()
{
return $this->latestReviewRequest->isPending();
}
}
Conditional eager-loads
An application may have publicly-available pages showing additional components to admins, requiring more Eloquent with()
eager-loads to avoid N+1 database queries.
return view('articles.index')
->with('articles',
Article::published()
->when(optional(Auth::user())->hasRole('admin'), function ($query) {
return $query->with('analytics');
})
->paginate()
);
optional
limitation – good for one arrow operator
Like Collection
proxy-enabled methods, you may only invoke a method or property one level deep on the object made optional()
.
class User extends Model
{
public function phone()
{
return $this->hasOne(Phone::class);
}
}
class Phone extends Model
{
public function isNorthAmerican()
{
return in_array($this->country, ['CA', 'US']);
}
}
optional(Auth::user())->phone->isNorthAmerican()
would cause an ErrorException
to occur. Guests make optional(Auth::user())->phone
return null
.
Nested optional(optional(Auth::user())->phone)->isNorthAmerican()
should be avoided as the whole intent of this function is to make code more readable. That expression is a horrorshow. Not to mention the Law of Demeter overreach happening here!
So as a general rule of thumb look up the origin of that phrase, only use a single arrow operator off optional()
PHP 7 null coalesce operator
Without a method call in an expression, you can avoid requiring the use of optional()
to access nested object properties. {{ Auth::user()->phone->number ?? null }}
silently echoes null
even when the value of Auth::user()->phone
is null
.
PHP 8 nullsafe operator
However the null coalesce operator doesn’t cover a chained expression containing a method call. PHP 8 introduces a nullsafe operator that uses the ?
character to indicate nullable properties. For expression Auth::user()?->phone?->isNorthAmerican()
, if Auth::user()
or Auth::user()->phone
evaluate to null
, the whole expression will resolve to null
.