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.