Easier Laravel polymorphic relationships with MorphMap
Polymorphic relationships are a hugely powerful way to make connections between tables in a Laravel application.
Say you have tables called companies
, users
, & admins
and all three needed to store addresses, without polymorphic relationships, we’d either have to forego standard two-way relationships in an addresses table and just use the ID, or we’d have to have three separate addresses tables, or even have all of our address fields on each of our three tables.
All carry the weight of redundancy, repetition and unnecessary complexity.
Written By
Craig Riley
Craig is a full-stack developer who mainly deals in JavaScript and PHP. Frameworks include VueJS Laravel, Wordpress, Nuxt and Quasar. He also likes sysadmin stuff until it goes horribly wrong.
Thankfully, with polymorphic relationships, we can simply use an addressable_type
and an addressable_id
column in our addresses table, and now we can maintain two-way relationships between companies
and addresses
; users
and addresses
; as well as admins
and addresses
.
Quick Recap
You’ll forgive us for having a very truncated “things you probably already know” section here, but here’s a quick code sample of how we’d go about setting up our relationships from the intro.
1return new class extends Migration2{3 public function up(): void4 {5 Schema::create('addresses', function (Blueprint $table) {6 $table->id();7 $table->morphs('addressable');8 // ...9 $table->timestamps();10 });11
12
13 // There's nothing special about the 'companies', 'admins', or 'users' table14 Schema::create('companies', function (Blueprint $table) {15 $table->id();16 // ...17 $table->timestamps();18 });19 }20}
And then for our models:
1// App\Models\Address2class Address extends Model3{4 public function addressable(): MorphTo5 {6 return $this->morphTo();7 }8}9
10// App\Models\User11class User extends Model12{13 public function addresses(): Relation14 {15 return $this->morphMany(Address::class, 'addressable');16 }17}18
19// And repeat above for Company and Admin
Why use Morph Maps?
If you notice in your addresses
database table, if you store a polymorphic relationship, you’ll have an addressable_type
of something like “App\Models\Company” and an addressable_id
of, say, “1”. Laravel is using the full class namespacing to resolve that relationship.
With Laravel being a very solid framework, this works fine of course. But what if you need to move that Company
model somewhere down the line. Maybe you want to extract some app functionality into a Laravel package and Company now needs a different namespace? Maybe you want to add multi-tenancy and this requires a deeper folder structure. Either way, your relationship is now broken and you’ll need to update every single entry of addressable_type
from “App\Models\Company” to “CompanyName\Package\Models\Company”.
A Morph Map sidesteps this issue by using an alias to the class in your database ‘_type’ column and defining the resolution of that alias in a provider boot()
function:
1use Illuminate\Database\Eloquent\Relations\Relation;2 3Relation::morphMap([4 'user' => 'App\Models\User',5 'company' => 'App\Models\Company',6 'admin' => 'App\Models\Admin'7]);
can simply become:
1use Illuminate\Database\Eloquent\Relations\Relation;2 3Relation::morphMap([4 'user' => 'CompanyName\Package\Models\User',5 'company' => 'CompanyName\Package\Models\Company',6 'admin' => 'CompanyName\Models\Admin'7]);
and your database can remain untouched with its addressable_type
of “user”, “company” or “admin”.
Using polymorphic relationships dynamically
Another benefit this approach offers us is making it much easier to create, edit or delete relationships from our frontend without having to create separate routes, controllers and services for each of our ‘admin’, ‘user’ and ‘company’ models.
We only need to create a single address
route for each of our CRUD functions:
// routes/web.phpRoute::post( '/addresses/{type}/{id}', [AddressController::class, 'store'])->name('addresses.store');
and inside our AddressController
controller, we can resolve our model like so:
class AddressController extends Controller{ use IdentifiesModel;
public function store(Request $request, string $type, int $id) { $model = $this->identifyModel($type, $id);
$validated = $request->validate([ 'line_1' => ['required','string'], 'line_2' => ['nullable','string'], 'city' => ['required','string'], 'country' => ['required','string'], 'postcode' => ['required','string','max:8'] ]);
$model->addresses()->create($validated); }}
// App\Traits;
trait IdentifiesModel{ protected function identifyModel(string $type, int $id): ?Model { $modelClass = collect(Relation::morphMap())->get($type); return $modelClass::findOrFail($id); }}
Putting it all together
Finally, we direct our POST request to “/addresses/company/74” and we have ourselves a future-proof route for creating addresses as polymorphic relationships on any number of other models.
Laravel will map the “company” in our route to “{type}” in our route declaration and “74” to “{id}” and then our trait will resolve the model instance with that information.
To extend this functionality to other models, we just need to import the IdentifiesModel
trait and call it, while ensuring our models are in the MorphMap from our provider. Because we’re using our much shorter and more stable alias, we don’t need to worry about passing the full class path (and escaping the backslashes) from our frontend either.
Where next?
There are ways we could expand this concept to make it even easier to maintain. For example, rather than using a trait to resolve each model in the controller, it’s probably possible to create a RouteBinding callback that handles the resolution automatically for us. We’re looking into that one now.
Also, rather than having to maintain a morph map in our provider, we could do this at a Model-level with the aid of an abstract model class we extend:
1abstract class Model extends BaseModel2{3 public function getMorphClass()4 {5 $class = get_class($this);6 throw new Exception("Model `{$class}` hasn't implemented `getMorphClass` yet");7 }8}
Now any model without a defined getMorphClass
function will immediately throw an exception and ensure that we haven’t missed one:
class Company extends Model{ public function getMorphClass() { return 'company'; }}
Laravel v8.59.0 brought an alternate way of ensuring all of our models with polymorphic relationships have defined map aliases which might be even easier. Rather than calling Relation::morphMap([])
, we instead opt for Relation::enforceMorphMap([])
.
More Tutorials
Here are some more tutorials that we think might be helpful for you. Feel free to contact us if there's anything you might need
Automating your service and repository patterns in Laravel with command generators
"Why spend 20 seconds doing something when you can spent 4 hours automating it," goes the proverb. See any professional proverbists around any more? No, because they've all been automated. Also it's fun. Whatever your programming pattern in Laravel, chances are that the php artisan make:x command is going to leave you high and dry on occasion. That's why it can be useful to have your own commands available to cover those gaps. We're going to go with the example of the "Service" pattern below.

Why you should be using backed enums in Laravel
Another year, another new "killer" feature for the poor, embattled software developer to learn rears its ugly head. Fortunately, enums only take a few minutes to learn and can be pretty useful for standardising database columns. Are they going to change the world? Probably not. Are they going to make a few problems slightly less problem-y? Quite possibly.

PHP Code Snippets Episode 2 - Use An Array To fill a Option Box
In this post I am going to show you how to take the values from an array and use them, in a form option box.
Passing through slots in Vue
If you're keeping your Vue component sizes small, there's a good chance you'll need to implement a wrapper component at some point. If you're only utilising the default slot for these, it can be as simple as putting a <slot /> tag inside your wrapper component. However, there's a bit more effort involved if you need to pass through named slots to your base component
How to Create a Simple Button Component in Figma
In this tutorial, we’ll create a simple button using Figma’s built-in component system.