Laravel 5.5 — Optional class, withDefault() and $attribute defaults

Laravel 5.5 is out! and with it, a new `Optional` class (with optional() helper) that is one of those “why didn’t someone think of this years ago?” improvements.

I was intending at first to go into that class more deeply, but Laracasts does a pretty good job already, so if you haven’t seen this mentioned yet it’s worth having a quick look at that (4min). Briefly, though, it works like this:

Image a User that may not yet have a Profile associated with them. So the normal relationship is set up:

function profile(){
return $this->hasOne('\App\Profile');
}

but when you call \App\User::find(1)->profile->age; it throws the old “ Trying to get property of non-object” error because there is no profile.

The point of the optional() class and helper is to prevent that blowup — by using:

optional(\App\User::find(1)->profile)->age

instead, if no profile object is returned (remember — we are taking advantage of our ORM to work with objects, instead of raw query records) it will “fail gracefully” and return a null instead.

There will be some debate about when and where this is appropriate to do; I would avoid running around slapping it on everything like an error suppressor. I can definitely see using this in my transformers, presenters and blades, however — probably any place where I am simply displaying and would like it to just be blank if empty.

One thing about this helper, though, is there is no way to pass in a default value; it is null and that’s it. If you want those, we need to rely on our existing functionality- withDefault() or using $attributes.

Somewhere out on my Laravel newsletter I covered these, but good time to review and see how they work with this new function. In our User model, let’s add a second relationship so they look like this:

function profile(){
return $this->hasOne('\App\Profile');
}

function defaultProfile(){
return $this->hasOne('\App\Profile')->withDefault([
'age' => 21
]);
}

We have two users in our system. User 1 has a profile record, but the age has not been set (and db record defaults to null). User 2 has no profile record at all.

$user = \App\User::find(1);
dd($user->profile->age); // null
dd($user->defaultProfile->age); // null
dd(optional($user->profile)->age); //null
dd(optional($user->defaultProfile)->age); // null

The second case is the one that might trip you up. It would be natural to assume that since the age field is null, it will be assigned the default value of “21”. However, ->withDefault() works when the relationship is null, not the field values. See how this contrasts with User 2, which does not have a Profile record yet.

$user = \App\User::find(2);
dd($user->profile->age); // error
dd($user->defaultProfile->age); // 21
dd(optional($user->profile)->age); //null
dd(optional($user->defaultProfile)->age); // 21

So, in short, this will let you create on-the-fly default objects to link. A place you might use this would be when you have different types of users that can register, and you’d like to essentially encapsulate the defaults for a new “X type” rather than copy/paste create() logic everywhere.

What about that $attributes thing I’ve mentioned a couple of times? You may not have even realized, but you can create default models with this:

class Profile extends Model
{
protected $attributes = ['age' => 19];
}

Now every profile will default to age 19 unless you specify a value. But…this only seems to work when a new Profile is instantiated, not with the code above. So to take advantage, you would need to do something like:

\App\User::find(2)->profile()->save(new \App\Profile());

Now when you look at your data (after refreshing) you’ll see the age is set to 19.

Hope that helps clear some thing up! Spend a little time playing with these — they can really help cut out a lot of messy boilerplate default logic from your code.