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.
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:
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:
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).
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:
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.
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:
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:
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:
classAddress{
publicfunctionaddressable(){
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.
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.