twitter rss github stackoverflow
Joseph Silber's Avatar

Joseph Silber

How to rid your database of PHP class names in Eloquent's Polymorphic tables

Polymorphic relations let you set up a relationship between many different model types, without the need for extra tables. This works by storing the "morphable type" (explained below) in the database, in addition to the morphable type's ID.

By default, the morphable type stored in the database is the model's full class name. While this works, it tightly couples your database to your PHP application. Let's look at how we can instruct Eloquent to use more generic values for these morphable types.

A short refresher on polymorphic relationships

If you're already familiar with polymorphic relationships, you can skip right ahead.

Polymorphic relationships are one of Eloquent's most powerful features. Let's take a quick look at what they are and why we'd want to use them:

Imagine we have an addresses table that stores our customer's addresses:

Table name:
  addresses

Columns:
  id
  customer_id
  street_number
  street_name

The customer_id column points to a given record in the customers table, connecting the address to the customer via the customer's ID. That's a pretty standard SQL relationship.

Using Eloquent, we set up the relationship in the Customer model using a HasOne relationship:

class Customer
{
    public function address()
    {
        return $this->hasOne(Address::class);
    }
}

This works great if Customer is the only entity type in our app that has an address. What if we have other entities in our app, such as Warehouse, that can also have an address? We don't want to create a separate table for warehouse addresses; we'd like to use the same addresses table for both customer and warehouse addresses.

This is what polymorphic relations are for. In addition to storing the foreign record's ID in the table, we'll now also store which type of entity the address belongs to:

Table name:
  addresses

Columns:
  id
- customer_id
+ addressable_id
+ addressable_type
  street_number
  street_name

Note that we...

  1. Renamed the customer_id column to addressable_id, since an address can now belong to any entity type (the addressable prefix (called the morph name) can really be anything; pick whatever makes sense in your context).

  2. Added the addressable_type column, which will tell us which entity type this address belongs to.

Next, we need to update our Customer model to use a MorphOne relationship:

class Customer
{
    public function address()
    {
        return $this->morphOne(Address::class, 'addressable');
    }
}

...and do the same for our Warehouse model:

class Warehouse
{
    public function address()
    {
        return $this->morphOne(Address::class, 'addressable');
    }
}

Now we can store both the customer and warehouse addresses in the same table, and Eloquent will manage it all for us behind the scenes:

$customer->address  // Both of these now work.
$warehouse->address // Polymorphic relations FTW!

This was a quick crash course on polymorphic relations. Now onto the crux of this article.

The default Morph Type

In Eloquent parlance, the "morph type" is the value that is stored in the {morph}_type column (in our example, this is the addressable_type column).

By default, Eloquent will store the model's full class name as the morph type.

Using our example above, let's assume we have two addresses: one for a Customer with an ID of 34, and another for a Warehouse with an ID of 83. Looking at our addresses table, we should see something like this:

id addressable_id addressable_type street_number street_name
1 34 App\Customer 109 Old Mill Rd.
2 83 App\Warehouse 2 Willow Rd.

As we can see, the App\Customer and App\Warehouse values are stored directly in the database. This is how Eloquent knows which entity type a given address belongs to.

This works, but it tightly couples the database to our PHP application's internal structure. If we have other systems accessing this database, the value App\Customer has no meaning to them.

What we really want is a way for us to instruct Eloquent what value to use for each entity type in the addressable_type column.

Customizing the Morph Type

Using the morphMap method on Eloquent's Relation class, we can instruct Eloquent to use a custom name for each entity instead of the class name.

We're looking for a value that would make sense to another system accessing this database. Using the foreign entities' table names sounds like a good approach.

We'll add this piece of code in the AppServiceProvider's boot method:

use Illuminate\Database\Eloquent\Relations\Relation;

public function boot()
{
    Relation::morphMap([
        'customers' => 'App\Customer',
        'warehouses' => 'App\Warehouse',
    ]);
}

With this in place, our scenario above would generate the following rows in the database:

id addressable_id addressable_type street_number street_name
1 34 customers 109 Old Mill Rd.
2 83 warehouses 2 Willow Rd.

Our PHP class names are now nowhere to be found. We've successfully decoupled our database from our application code!

Bonus: Since using the foreign table names is such a common practice, you can actually omit the keys when setting the morph map, and Eloquent will automatically use the model's table name as the morph type:

Relation::morphMap([
    'App\Customer',
    'App\Warehouse',
]);

Why doesn't Eloquent do this automatically?

After realizing the benefits of decoupling the database from the application structure, the natural question arises: why doesn't Eloquent default to using the model's table name for the morph type?

The answer to this lies in the inverse relationship.

So far, we've only looked at the code in our Customer and Warehouse models, which allows us to get at their address with $customer->address or $warehouse->address. But what about the other way around? What if we have an $address, and we want to get the related model?

Let's add the MorphTo relationship to our Address class:

class Address
{
    public function addressable()
    {
        return $this->morphTo(); // Yes. No arguments :)
    }
}

With the relationship in place, we can now access the related model via the addressable attribute on any address:

Address::first()->addressable; // A Customer or a Warehouse

Pretty neat!

Now let's think back to our original question: why doesn't Eloquent default to using the model's table name for the morph type?

The answer is that it's simply impossible. If Eloquent would've always used the table name by default, the following code would indeed properly save the addressable_type as customers:

Customer::first()->address()->create([...]);

...but if we later have an Address instance and want to know which entity it belongs to, how would a customers value in the addressable_type column help us? There's nothing that connects the App\Customer class to the customers morph type.

Since PHP classes are lazy loaded on demand, there's no outstanding list of all classes in the app. There's no way for Eloquent to know on its own where all of your models are located, or what their class names are.

That's why we need to register our models with Relation::morphMap(), so that we can always know which entity a given morph type is associated with.

In closing

To fully decouple your database from your app's structure, always register all of your models with Relation::morphMap(), and live happily ever after.


Questions? Comments? Complaints? Ping me on Twitter.

Recent posts

  1. Releasing Bouncer: Roles and Permissions for Laravel apps

    A personal post with some musings about my journey through the years - from inception to final release

    Read more ➺

  2. Lazy Collections in Laravel

    A deep dive into Laravel's Lazy Collections, and how to process enormous amounts of data with very little memory.

    Read more ➺

  3. Getting the current user (or other session data) in a Laravel controller constructor

    Session data is not available directly in controller constructors. Let's see how we can work around that.

    Read more ➺

  4. Gate and authorization improvements in Laravel 5.3

    With Laravel 5.2 we got a nice authorization system right out of the box. In 5.3 this got a lot of improvements and refinements. Let's take a look.

    Read more ➺

  5. The new Closure::fromCallable() in PHP 7.1

    Let's take a look at a neat little feature coming in PHP 7.1: easily converting any callable into a proper Closure using the new Closure::fromCallable() method.

    Read more ➺