0

I'm dealing with a challenge where I want to create a polymorphic model Localization. This will include long-text columns like default, en_us, de_de,...

Now imagine another model Product. Products usually have text attributes like name, description,... Here comes the part, where I could use a polymorphic relation with Localizations. The localizations table is supposed to have this type of structure:

id localizable_id localizable_type default en_us de_de
1 25 App\Model\Product Phone null Telefon
2 25 App\Model\Product The best phone on the market without an audio jack null Das beste Telefon auf dem Markt ohne Audioanschluss
3 15 App\Model\Article Top 10 products null Top 10 Produkte
4 15 App\Model\Job Salesman null Verkäufer

If I want a Product to have name and description localizable, then according to the Laravel documentation, you should expect something like this in my code:

class Product extends Model
{
  public function name() // Expecting model with default attribute 'Phone'
  {
    return $this->morphOne(Localization::class,'localizable');
  }

  public function description() // Expecting model with default attribute 'The best phone on the market without an audio jack'
  {
    return $this->morphOne(Localization::class,'localizable');
  }
}

No matter how obvious it is, it won't work correctly, because I can't expect two identical methods to return different values.

On the other side, if I wanted to follow Laravel's convention, the Localization model is supposed to look like this:

class Localizable extends Model
{
  public function localizable()
  {
    return $this->morphTo(__FUNCTION__,'localizable_type','localizable_id');
  }
}

As you can see in the example above, the polymorphic relation is inverse to what I need. The problem is that I want one table with all the strings, which might be translated (localizables) and use them in many other models than just the Product, like Shop, Job, Article,...

I need to somehow implement the distinction not only between localizable_types but also on which column/attribute it is supposed to be related. What is the best approach to achieve this?

  • Why not use Laravel's built-in localization support? If you want to be able to edit the translation files via web interface, there are packages like this one. https://github.com/barryvdh/laravel-translation-manager – miken32 Jan 08 '21 at 18:11
  • @miken32 first I need to be able to create specific packages for translating agencies (which is not possible via laravel’s localizations). Second I need to separate app localization from data localization. Third it does not answer the question in general - another example might be applied to images, where one user can have a profile image and a background image (with polymorphic images for use in e.g. articles) – David Sojka Jan 08 '21 at 18:31
  • Wouldn't simply adding another column to the table with the type (name, description, etc.) work? Then filter the relationship based on it? – miken32 Jan 08 '21 at 18:36
  • @miken32 I thought of column localizations.localizable_context which would work for retrieving existing records, but I would have to somehow modify the Localization’s save() method to add this relation name to it. And I don’t how to access such information from the save method – David Sojka Jan 08 '21 at 18:58
  • https://stackoverflow.com/questions/63610956/how-to-save-additional-extra-columns-of-the-polymorphic-morphable-model-in-larav – miken32 Jan 08 '21 at 20:45
  • @miken32 that is not the answer. I have 1:1 relation, which is not that much of a difference. But more importantly, this approach would require to explicitly specify the relation type for every new instance - that seems to be prone to bugs. – David Sojka Jan 08 '21 at 21:00

1 Answers1

1

I finally found a workaround that fulfills my expectations. Even though it is not the most beautiful (or Laravel) way to do it. I got inspired from a similar struggle that I've found on GitHub here.

The point of this solution is to override Product's getMorphClass() method that is being used to determine the *_type column value. My Product model:

class Product extends Model
{
  protected $morphClass = null; // Create an attribute that will not be saved into the DB
  public function getMorphClass() // Method for determinating the type value
  {
    return $this->morphClass? : self::class;
  }

  public function name()
  {
    $this->morphClass = self::class . '.name'; // App\Models\Product.name
    return $this->morphOne(Localization::class, 'localizable');
  }

  public function description()
  {
    $this->morphClass = self::class . '.description'; // App\Models\Product.description
    return $this->morphOne(Localization::class, 'description');
  }

}

The modification above will affect, how Laravel is going to save the Localizations to the database. Now, some work needs to be done from the other side of the relations - custom polymorphic types. Update your AppServiceProvider class in the app/Providers/AppServiceProvider.php file. You have to add morphMap to the boot() method:

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
  // ...any of your previous modifications...

  Relation::morphMap([
    Product::class . '.name' => Product::class,
    Product::class . '.description' => Product::class,
  ]);
}

BUT BE AWARE: Once you decide to use multiple same-type morphs in your model, you have to change the protected attribute $morphClass in every dynamic method before calling morphOne() or morphMany(). Otherwise you might be saving descriptions to names or vice versa.

Also, when you will use the morphTo() method on the other side, you will only get the Product - if you need to know, what attribute on the Product model is being related on, I recommend you to create some method for extracting the context.

Please feel free to comment any potential threats in this approach.