Efficient appended property using Laravel Eloquent

 Reading time ~2 minutes

Heads up: this article is over a year old. Some information might be out of date, as I don't always update older articles.

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();
comments powered by Disqus