Re-Introducing Eloquent’s Polymorphic Relationships
This article was peer reviewed by Younes Rafie. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
You’ve probably used different types of relationships between models or database tables, like those commonly seen in Laravel: one-to-one, one-to-many, many-to-many, and has-many-through. But there’s another type of relationship that’s not so common: polymorphic. So what is a polymorphic relationship?
A polymorphic relationship is where a model can belong to more than one other model on a single association.
To clarify this, let’s create an imaginary situation where we have a Topic
and a Post
model. Users can leave comments on both topics and posts. Using polymorphic relationships, we can use a single comments
table for both of these scenarios. Surprising, yeah? This seems a bit impractical since, ideally, we’d have to create a post_comments
table and a topic_comments
table to differentiate the comments. With polymorphic relationships, we don’t need two tables. Let’s look into polymorphic relationships through a practical example.
What We’ll Be Building
We’ll be creating a demo music app which has songs and albums. In this app, we’ll have the option to upvote both songs and albums. Using polymorphic relationships, we’ll use a single upvotes table for both of these scenarios. First, let’s examine the table structure required to build this relationship:
albums
id - integer
name - string
songs
id - integer
title - string
album_id - integer
upvotes
id - integer
upvoteable_id - integer
upvoteable_type - string
Let’s talk about the upvoteable_id
and upvoteable_type
columns which may seem a bit foreign to those who’ve not used polymorphic relationships before. The upvoteable_id
column will contain the ID value of the album or song, while the upvoteable_type
column will contain the class name of the owning model. The upvoteable_type
column is how the ORM determines which “type” of owning model to return when accessing the upvoteable
relation.
Generating the Models Alongside Migrations
I am assuming you already have a Laravel app that’s up and running. If not, this premium quick start course might help. Let’s start by creating the three models and migrations, then edit the migrations to suit our needs.
php artisan make:model Album -m
php artisan make:model Song -m
php artisan make:model Upvote -m
Note, passing the -m
flag when creating models will generate migrations associated with those models as well. Let’s tweak the up
method in these migrations to get the desired table structure:
{some_timestamp}_create_albums_table.php
public function up()
{
Schema::create('albums', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
}
{some_timestamp}_create_songs_table.php
public function up()
{
Schema::create('songs', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->integer('album_id')->unsigned()->index();
$table->timestamps();
$table->foreign('album_id')->references('id')->on('album')->onDelete('cascade');
});
}
{some_timestamp}_create_upvotes_table.php
public function up()
{
Schema::create('upvotes', function (Blueprint $table) {
$table->increments('id');
$table->morphs('upvoteable'); // Adds unsigned INTEGER upvoteable_id and STRING upvoteable_type
$table->timestamps();
});
}
We can now run the artisan migrate
command to create the three tables:
php artisan migrate
Let’s now configure our models to take note of the polymorphic relationship between albums, songs, and upvotes:
app/Upvote.php
[...]
class Upvote extends Model
{
/**
* Get all of the owning models.
*/
public function upvoteable()
{
return $this->morphTo();
}
}
app/Album.php
class Album extends Model
{
protected $fillable = ['name'];
public function songs()
{
return $this->hasMany(Song::class);
}
public function upvotes()
{
return $this->morphMany(Upvote::class, 'upvoteable');
}
}
app/Song.php
class Song extends Model
{
protected $fillable = ['title', 'album_id'];
public function album()
{
return $this->belongsTo(Album::class);
}
public function upvotes()
{
return $this->morphMany(Upvote::class, 'upvoteable');
}
}
The upvotes
method in both the Album
and Song
models defines a polymorphic one-to-many relationship between these models and the Upvote
model and will help us get all of the upvotes for an instance of that particular model.
With the relationships defined, we can now play around with the app so as to get a better understanding of how polymorphic relationships work. We won’t create any views for this app, we’ll just tinker around with our application from the console.
In case you are thinking in terms of controllers and where we should place the upvote
method, I suggest creating an AlbumUpvoteController
and a SongUpvoteController
. By this, we kind of keep things strictly tied down to the thing we are acting on when working with polymorphic relationships. In our case, we can upvote both albums and songs. The upvote is not part of an album nor is it part of a song. Also, it’s not a general upvote thing, as opposed to how we’d have an UpvotesController
in most one-to-many relationships. Hopefully this makes sense.
Let’s fire up the console:
php artisan tinker
>>> $album = App\Album::create(['name' => 'More Life']);
>>> $song = App\Song::create(['title' => 'Free smoke', 'album_id' => 1]);
>>> $upvote1 = new App\Upvote;
>>> $upvote2 = new App\Upvote;
>>> $upvote3 = new App\Upvote;
>>> $album->upvotes()->save($upvote1)
>>> $song->upvotes()->save($upvote2)
>>> $album->upvotes()->save($upvote3)
Retrieving the Relationships
Now that we have some data in place, we can access our relationships via our models. Below is a screenshot of the data inside the upvotes table:
To access all of the upvotes for an album, we can use the upvotes dynamic property:
$album = App\Album::find(1);
$upvotes = $album->upvotes;
$upvotescount = $album->upvotes->count();
It’s also possible to retrieve the owner of a polymorphic relation from the polymorphic model by accessing the name of the method that performs the call to morphTo
. In our case, that is the upvoteable
method on the Upvote model. So, we will access that method as a dynamic property:
$upvote = App\Upvote::find(1);
$model = $upvote->upvoteable;
The upvoteable
relation on the Upvote model will return an Album
instance since this upvote is owned by an instance of the Album
instance.
As it is possible to get the number of upvotes for a song or an album, we can sort the songs or albums based on the upvotes on a view. That is what happens in music charts.
In the case of a song we’d get the upvotes like so:
$song = App\Song::find(1);
$upvotes = $song->upvotes;
$upvotescount = $song->upvotes->count();
Custom Polymorphic Types
By default, Laravel will use the fully qualified class name to store the type of the related model. For instance, given the example above where an Upvote
may belong to an Album
or a Song
, the default upvoteable_type
would be either App\Album
or App\Song
, respectively.
However, there is one big flaw with this. What if the namespace of the Album
model changes? We will have to make some sort of migration to rename all occurrences in the upvotes
table. And that’s a bit crafty! Also what happens in the case of long namespaces (such as App\Models\Data\Topics\Something\SomethingElse
)? That means we have to set a long max length on the column. And that is where the MorphMap
method comes to our rescue.
The “morphMap” method will instruct Eloquent to use a custom name for each model instead of the class name:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'album' => \App\Album::class,
'song' => \App\Song::class,
]);
We can register the morphMap
in the boot function of our AppServiceProvider
or create a separate service provider. For the new changes to take effect, we have to run the composer dump-autoload
command. So now, we can add this new upvote record:
[
"id" => 4,
"upvoteable_type" => "album",
"upvoteable_id" => 1
]
and it would behave in the exact same manner as the previous example does.
Conclusion
Even though you’ve probably never run into a situation which required you to use polymorphic relationships, that day will likely eventually come. The good thing when working with Laravel is that it’s really easy to deal with this situation without having to do any kind of model association trickery to get things working. Laravel even supports many-to-many polymorphic relations. You can read more about that here.
I hope you’ve now understood polymorphic relationships and situations which may call for these types of relationships. Another, slightly more advanced example on Polymorphic relationships is available here. If you found this helpful, please share with your friends and don’t forget to hit the like button. Feel free to leave your thoughts in the comments section below!