Introduction
This technique is really useful to check if an Eloquent relation is loaded on a Model, in order to efficiently generate an appended property.
For example, this approach is particularly useful in a multilingual application, where you can have an entity that has a number of related translations in different languages. Consider the following code:
<?php
class Entity extends Model
{
public function translations()
{
return $this->hasMany('Translation');
}
}
Our Entity
class can have many translations, but the end user is interested only in the one that corresponds to his Locale. Wouldn’t be nice to automatically have a property in the class that stores the current translation or a fallback in case it doesn’t exists?
In Laravel we can assign appended properties to each model simply by adding them to the $appends
array:
<?php
protected $appends = ['translation'];
We have also to add a function named getTranslationAttribute
that returns our value. For example consider the following:
<?php
public function getTranslationAttribute()
{
$locale = App::getLocale();
if($translation = $this->hasMany('Translation')->where('locale', '=', $locale)->first()) {
return $translation->translation;
}
return $this->getAttribute('name');
}
Yeah this could do the work! It gets the locale of the user and tries to find a suitable translation by filtering all the translations. If the translation is not defined it falls back to the name of the entity.
However, when testing the application (laravel-debugbar FTW), you realize that this is not an efficient approach because it causes a large number of queries, one for each model.
What is wrong with my code?
You just slipped on the N+1 problem, a well-known data access antipattern where the database is accessed in a suboptimal way. You are making a new query to get the translation for each one of your Entity. You really have to eager load the data before trying to access it.
A solution can be editing the previous code as follows:
<?php
public function getTranslationAttribute()
{
$locale = App::getLocale();
if($this->isTranslationsLoaded())
{
foreach($this->translations as $translation) {
if($translation->locale == $locale) return $translation->translation;
}
}
return $this->getAttribute('name');
}
Where the function isTranslationsLoaded
is defined as the following:
<?php
public function isTranslationsLoaded() {
return array_key_exists('translations', $this->relations);
}
By doing so you are checking if the relation exists before trying to access the Translation model. Remember that you can eager load the relations by using the with
method:
<?php
$entities = App\Entity::with(['translations'])->get();