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 Migration
2{
3 public function up(): void
4 {
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' table
14 Schema::create('companies', function (Blueprint $table) {
15 $table->id();
16 // ...
17 $table->timestamps();
18 });
19 }
20}

And then for our models:

1// App\Models\Address
2class Address extends Model
3{
4 public function addressable(): MorphTo
5 {
6 return $this->morphTo();
7 }
8}
9
10// App\Models\User
11class User extends Model
12{
13 public function addresses(): Relation
14 {
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.php
Route::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 BaseModel
2{
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.

Copyright 2007 - 2024 southcoastweb is a brand of DSM Design.