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()

View the optional() function implementation

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.