Automatically generating your Laravel morph map
Make Laravel morph maps fun again!
Polymorphic relations in Laravel are a very cool feature! You can create a model like Comment
and link it to models like Post
and Article
. Such a link can be made by adding the fields commentable_id
and commentable_type
to the Comment
model.
When we want to link a comment to a post, we have to create a new Comment
model with commentable_id
equal to the id of the post and commentable_type
equal to App\Post
, the class of the post.
But what if we change the location of our Post
model from App\Post
to App\Models\Post
? In our database, the commentable_type
fields won't be updated, and thus Laravel cannot find the comments associated with our post since the App\Post
model doesn't exist anymore.
Yes, we could add a migration to update the values of commentable_type
with the correct post model, but that's quite a bit of work that can be quickly forgotten.
Laravel has an excellent solution to this: morph maps! Within your application, you register a mapping between the class of your model and a simple identifier. In our scenario, the morph map would look like this:
Relation::morphMap([
'post' => 'App\Post',
]);
In the database, we will now use post in the commentable_type
row instead of App\Post
. Whenever we want to move our Post model, the only thing we have to do is updating the morph map, neat!
Maintaining morph maps requires discipline
I cannot count the number of times I forgot to add an entry to a morph map, and the full class name of a model would be stored in the database.
We're working on a massive project these days with lots of models. And I know I'm going to forget to add some models to the morph map. My colleagues will forget to add some of these models. I think everyone sometimes will forget to add a model to the map.
Can't we do better? Is it possible to automatically generate a morph map, so we don't forget to add these models?
Dynamically generating morph maps
Okay, all we have to do is add a method to a model, so we know its morph identifier. Luckily, each Eloquent model already has a method getMorphClass
, which Laravel uses to save the model's morph identifier.
getMorphClass
will return the model's full class name when the model is not present in the morph map. When the model exists in the morph map, its identifier from the map will be returned. We overwrite this function in each model like this:
class Post extends Model
{
public function getMorphClass()
{
return 'post';
}
}
Cool! We're halfway done. When we save a comment linked to a post, it will use post
as commentable_type
and not the full class name.
But a morph map is still required when we want to load the post associated with the comment since Laravel doesn't know that post
is an App\Models\Post
model.
Creating such a map is now actually really simple. We can search for all the models in our application, run the getMorphClass
method on them. And we're done. We've got ourselves a dynamically generated morph map!
Making sure every model has a morph identifier
We can even take it a step further. It is still possible that someone forgets to add an implementation of getMorphClass
to a model. Let's fix that!
We'll make an abstract base model from which all our models will extend:
abstract class Model extends BaseModel
{
public function getMorphClass()
{
$class = get_class($this);
throw new Exception("Model `{$class}` hasn't implemented `getMorphClass` yet");
}
}
Now when a model doesn't implement getMorphClass
, an exception is thrown. And we know for sure each model has a morph identifier.
This won't create a lot of exceptions in production. Since at the boot process of our application, getMorphClass
will be called for each model. In our local setup, we'll know which models are missing a getMorphClass
implementation immediately.
Cool, we're done now we've got ourselves a dynamically generated morph map!
Package it up!
I've added this functionality into a new package called: laravel-morph-map-generator, which adds some extra functionality like caching dynamically generated morph maps on production, ignoring models, and more!
Let me know what you think about it.