Customize Model Accessor Over Null Object Pattern
As per Laravel documentation1, the belongsTo
, hasOne
, hasOneThrough
, and morphOne
relationships allow us to define a default model if no related model is found. This pattern is known as Null Object Pattern
in software engineering world. This can be very useful from time to time and it is very easy to achieve, thanks to the eloquence of Laravel.
Consider the following polymorphic relationship between the User
and the File
model for a profile picture.
|
|
Aside from traditional $this->morphOne(File::class, 'fileable')
, we are returning an empty File
object using withDefault()
method if no corresponding profile picture is found for the user. Let’s take a look inside our File
model.
|
|
Here we have pretty standard stuff. Some attributes in the $fillable
property, the polymorphic relationship fileable()
and an accessor url
to sort out the actual URL from the path of the file.
Without doing any additional thing to the withDefault()
method in User
model relationship, we would get an empty File model. Sometimes, this is enough. But often we need more than that. For example, in this case, we would want to generate some sort of SVG for missing profile picture which is a standard practice. We can do so:
|
|
We can pass an array inside withDefault()
with the attributes we want to populate. For the url
attribute, we are using my SVG Avatar Generator library for Laravel to generate an SVG on the fly based on the string we are passing.
But “Houston, we have a problem”2. All of our users now have same profile picture cause we are passing same “Default User” string. That’s underwhelming. How can we make it more exciting?
Turns out, Laravel makes it very effortless (yeah, the level of attention to detail is 🤯). Instead of array, we can pass a closure to the withDefault()
. The closure receives two parameters; first, the related model; and second, the parent model:
|
|
Great! Now all users have customized SVGs based on their names. 💅
url
but in the closure we set the value of path
. I don’t like to set an accessor value directly, I would rather generate the value based on the path. This is just a personal preference. If you do something like $file->url = Svg::for($user->name)->toUrl()
it would work as well.Also, we have to modify the url
accessor on the File
model slightly. Since we are setting the URL directly in path
, we can no longer just call Storage::url()
over another URL. There should be a check if the path
is a file path or a URL. For this we can use Laravel’s URL
facade.
|
|
That’s all. Now if we set a URL from the parent relation, it will not be accidentally overwritten by the accessor. Note that, there is no additional trickery outside Laravel, it’s just simple things like this what makes the framework beautiful.