Reputation: 117
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?
Upvotes: 0
Views: 3168
Reputation: 117
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.
Upvotes: 1