Re-Introducing Eloquent’s Polymorphic Relationships

Christopher Vundi
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!

Frequently Asked Questions on Eloquent’s Polymorphic Relationships

What are the benefits of using polymorphic relationships in Laravel?

Polymorphic relationships in Laravel provide a flexible and efficient way to handle related data across different models. They allow a model to belong to more than one other type of model on a single association. This means you can have a single list of unique identifiers for all related data, regardless of their type. This can greatly simplify your database structure and make your code more maintainable. It also allows for more dynamic and flexible data relationships, which can be particularly useful in complex applications.

How do I set up a polymorphic relationship in Laravel?

Setting up a polymorphic relationship in Laravel involves defining the relationship in your Eloquent models. First, you need to define the relationship on the model that will be receiving the polymorphic relation. This is done using the morphTo method. Then, on the model that will be relating to the other models, you use the morphMany or morphOne method, depending on whether the relationship is one-to-many or one-to-one.

Can you provide an example of a polymorphic relationship in Laravel?

Sure, let’s consider a blogging platform where both posts and users can have comments. In this case, the Comment model would have a polymorphic relationship with both the Post and User models. Here’s how you might define this relationship:

class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}

class Post extends Model
{
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}

class User extends Model
{
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}

How do I retrieve related records using a polymorphic relationship?

You can retrieve related records in a polymorphic relationship just like you would with any other Eloquent relationship. For example, if you wanted to retrieve all comments for a post, you could do so like this:

$post = App\Post::find(1);
$comments = $post->comments;

How do I save related records using a polymorphic relationship?

Saving related records in a polymorphic relationship is also similar to other Eloquent relationships. You can use the associate method to associate the models, and then save the model. Here’s an example:

$comment = new App\Comment(['body' => 'A new comment.']);

$post = App\Post::find(1);

$post->comments()->save($comment);

What are some common use cases for polymorphic relationships?

Polymorphic relationships are particularly useful in situations where a model can belong to more than one other type of model. Some common use cases include comments that can belong to both posts and users, tags that can be applied to multiple types of content, and images or files that can be attached to various types of entities.

Are there any limitations or drawbacks to using polymorphic relationships?

While polymorphic relationships offer a lot of flexibility, they can also be more complex to set up and manage than standard Eloquent relationships. They also don’t support all the features of standard relationships, such as eager loading constraints.

How do I delete related records in a polymorphic relationship?

You can delete related records in a polymorphic relationship using the delete method on the relationship. For example, to delete all comments for a post, you could do so like this:

$post = App\Post::find(1);
$post->comments()->delete();

Can I use polymorphic relationships with many-to-many relationships?

Yes, Laravel supports many-to-many polymorphic relationships through the morphToMany and morphedByMany methods. This allows a model to belong to multiple models on a many-to-many basis.

How do I handle polymorphic relationships in database migrations?

In your database migrations, you would typically add two columns to the table that will be receiving the polymorphic relation: one for the related model ID, and one for the related model type. Laravel provides a convenient morphs method to add these columns:

$table->morphs('commentable');
This will add commentable_id and commentable_type columns to the table.