Re-Introducing Eloquent’s Polymorphic Relationships

Christopher Vundi
Share

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!


Laravel logo

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:

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!