简体   繁体   中英

Laravel 5.2 Eloquent - how to automatically fetch translated content using separate translations table

I am wondering how I can make Eloquent automatically fetch translated content from the database for me, without me having to specify it. For example, instead of having to run something like the following:

$article = Article::someTranslationMethod("en")->first(); //to get the English version
$article = Article::someTranslationMethod("de")->first(); //to get the German version
$article = Article::someTranslationMethod()->first();     //to get the current locale version
//etc

I would like to be able to simply run:

$article = Article::first();

and have the result already translated - according to the current locale or whatever.

Details:

My database schema consists of an Articles table, and a Translations table. (Please excuse my German):

Articles
id  |           title            |                 body
------------------------------------------------------------------------
 1  |  How to drink your coffee  |  Hi, this is an article about coffee.

Translations
id  |  language  |  remote_table  |  remote_id  |  remote_field  |  value  
--------------------------------------------------------------------------------------------------------
 1  |        en  |      articles  |          1  |         title  |  How to drink your coffee
 2  |        de  |      articles  |          1  |         title  |  Wie sollte man seinen Kaffee trinken
 3  |        en  |      articles  |          1  |          body  |  Hi, this is an article about coffee.
 4  |        de  |      articles  |          1  |          body  |  Hallo, dieser Text geht um Kaffee.

So when the locale is set to "en": $article->title should have the value "How to drink your coffee" , but when the locale is set to "de": $article->title should have the value "Wie sollte man seinen Kaffee trinken" .

Using ActiveRecord in the past, I was able to achieve this simply by running a function right after the result was fetched from the database. It would look into the translations table for translations, and replace the $article object's properties with their translations. And that was it. Every time I had to fetch an article, I would run something like $article = MyActiveRecord::FindFirst('articles'); , and I would be confident that the result already holds the translated values, without me having to take any further action to achieve this - which is exactly what I am after.

But using Laravel with Eloquent, this is not possible anymore. At least, not out of the box. (And even if it was, that would mean changing Eloquent's source code - which is not an option). Therefore, it has to be manually implemented. Right now I can think of three possible approaches:

  • One possible approach is to have a custom base model that extends the Eloquent\\Model class, then have all my models extend the custom one, and in that custom model somehow add this functionality. Problem is, I do not know how (and if) I can add this functionality.

  • Another way would be to use an Eloquent relationship . So something like $article->hasMany("translations") . The problem in this case is that, in order to specify such a relationship, one foreign key is not enough, as the translations table holds the translations for every other table in the database. So an article is represented in the translations table not only by its ID, but by a combination of remote_table , remote_id and remote_field . And even if I could somehow create this relationship, I would probably still have to take further action - the translated content would indeed be included in the result, but $article->title would still have the English value for example.

  • A third approach that I found while searching is to use accessors and mutators . Although I am not experienced with them, I do not see how they can solve this problem, as it is not a question of data formatting, but rather of replacing it with data from another table. Plus, they do not rid me of having to take further action after fetching the data.

My bets would go to extending the Eloquent\\Model class, and somehow making it happen in there. But I do not see how.

Any thoughts will be greatly appreciated.

I think you can achieve this with a global scope applied to all Article queries.

Open up your Article model and then locate the public static function boot() (or create it if it does not exist)

public static function boot()
{
    parent::boot();

    static::addGlobalScope('transliterated', function(Builder $builder){
        return $builder->translate(app()->getLocale());
    });
}

This should work out of the box for you and apply to all queries made against Article::

The solution that finally did the trick was to use an Eloquent Event . I did indeed create a custom base model class that extends the Eloquent\\Model, and what I did was to register a new Eloquent Event to it, called "loaded", which is triggered right after a model instance is created. So now the result can be manipulated accordingly, and returned to our application already translated.

This is my custom base model, where I am registering the custom "loaded" event:

<?php

//app/CustomBaseModel.php

namespace App;

use Illuminate\Database\Eloquent\Model;

abstract class CustomBaseModel extends Model
{
    /**
     * Register a loaded model event with the dispatcher.
     *
     * @param  \Closure|string  $callback
     * @return void
     */
    public static function loaded($callback)
    {
        static::registerModelEvent('loaded', $callback);
    }

    /**
     * Get the observable event names.
     *
     * @return array
     */
    public function getObservableEvents()
    {
        return array_merge(parent::getObservableEvents(), array('loaded'));
    }

    /**
     * Create a new model instance that is existing.
     *
     * @param  array  $attributes
     * @param  $connection
     * @return \Illuminate\Database\Eloquent\Model|static
     */
    public function newFromBuilder($attributes = array(), $connection = NULL)
    {
        $instance = parent::newFromBuilder($attributes);

        $instance->fireModelEvent('loaded', false);

        return $instance;
    }
}//end class

Then I had to tell the application to listen for this event, so I created a new Service Provider for this purpose:

<?php

//app/Providers/TranslationServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;

class TranslationServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot(DispatcherContract $events)
    {
        $events->listen('eloquent.loaded: *', function ($ourModelInstance) {  // "*" is a wildcard matching all models, replace if you need to apply to only one model, for example "App\Article"

            //translation code goes here
            $ourModelInstance->translate();  //example
        }, 0);
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

I registered the new Service Provider to the app:

<?php

//config/app.php

...

'providers' => [

     ...

     /*
      * Custom Service Providers...
      */

     App\Providers\TranslationServiceProvider::class,
],

...

Finally, for all the models that we want to be able to use the "loaded" event with, we extend them from the CustomBaseModel class, instead of the Eloquent\\Model one:

<?php

//app/Article.php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Article extends CustomBaseModel
{
    //
}

And voila. Every time we load an article, the "loaded" event is triggered, so we can do whatever we want with it, and then return it to the app.

Side notes:

  • A debate on github about why this event is not already included can be found here ,
  • I originally got the idea from this post .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM