Enforcing Morph Maps in Laravel
Published on by Aaron Francis
Oliver Nybroe contributed a pull request to Laravel (8.59.0) that allows developers to require that morph maps to be set, rather than defaulting to fully qualified class names.
By using the enforceMorphMap
method instead of the traditional morphMap
method, Laravel will ensure that all morphs are mapped to an alias and throw a ClassMorphViolationException
exception if one is not mapped.
// If any other models are morphed but not mapped, Laravel// will throw a `ClassMorphViolationException` exception.Relation::enforceMorphMap([ 'user' => User::class,]);
You can enforce a morph map using the single call shown above, or you can use the standalone requireMorphMap
method on the Relation
class:
// Turn morph map enforcement on (new in 8.59.0).Relation::requireMorphMap(); // And then map your morphs in the standard way.Relation::morphMap([ 'user' => User::class,]);
Morph Map Background
When creating polymorphic relationships in Laravel, the default behavior has always been to store the class name of the related model in the database. From the Laravel documentation:
By default, Laravel will use the fully qualified class name to store the "type" of the related model. For instance, given the one-to-many relationship example above where a Comment model may belong to a Post or a Video model, the default
commentable_type
would be eitherApp\Models\Post
orApp\Models\Video
, respectively.
Using this default method means that your database will end up populated with the class names of your models, which tightly couples the data in your database to the names of your classes.
Laravel has always given us the ability to decouple the class name from the database by registering a MorphMap which provides an alias for a class, breaking that association:
// Store `user` in the database, instead of `App\User`Relation::morphMap([ 'user' => User::class,]);
While this behavior has been available for some time, it has never been possible to strictly require it.
Benefits of Morph Maps
The benefit of having a morph map in the first place is to decouple your application logic from your stored data. Storing class names in your database can lead to pernicious, hard to debug errors.
If you change the name of one of the classes that has been morphed, all of the references in your database will no longer line up with your application. This sneaky part about this is that it's likely that nothing will fail in development or testing! Unless your test suite somehow populates your database with the previous class name, all of your tests will be green. Only when you deploy to production will the class and data mismatch arise.
The scenario above could be solved by writing a migration to update your stored data, but there's no guarantee that you (or the next person, or the next) will remember that it's necessary.
Rather than relying on your company's institutional memory, morph maps break the association and free you from that potential bug.
Benefits of Enforcing A Morph Map
In the same way that morph maps free you from having to remember to update stored data when you refactor a class, requiring a morph map frees you from having to remember that you need to register a class in the first place.
Before morph maps were enforceable, it was incumbent on the developer or the team to remember to map morphs in the first place.
For example, one developer could set up a morph map like this:
Relation::morphMap([ 'image' => Image::class, 'post' => Post::class,]);
and then another developer could come along and start using App\Video
as a morphed relation without adding it to the map!
Now, in the database, you'd see the following types:
-
image
- from the morph map -
post
- from the morph map -
App\Video
- from the Laravel default implementation
Because the developer didn't know there was a morph map set up, they didn't register their new Video
model. Given an old enough application or a large enough team, this is an inevitability.
Now by enforcing a morph map, the App\Video
cannot be used in a morph unless an alias has been set up. A ClassMorphViolationException
will be thrown.
// All future morphs *must* be mapped!Relation::enforceMorphMap([ 'image' => Image::class, 'post' => Post::class,]);
This new feature allows you take what was implicit knowledge and make it an explicit code requirement.
The more explicit we can make the requirements of an application the easier it will be to maintain over time, and the easier it will be to bring on new developers.
Writing and tweeting about building products + Laravel.
Working on torchlight.dev & hammerstone.dev.
Dad to boy/girl twins.