Adewale Abati Web engineer, tech lifestyle YouTuber, public speaker. Building communities and open source for the Next Billion Users.

Polymorphic relationships in Laravel and their use cases

6 min read 1682

polymorphic relationships laravel feature image

Editor’s Note: This post was updated in September 2021 for accuracy and to include information on multiple types of polymorphic relationships in Laravel, including one-to-one, one-of-many, and many-to-many relationships.

It’s not uncommon in software development to have models that can belong to more than one entity. This type of model maintains the same structure that doesn’t change regardless of the other model it’s connected to.

A common example of this kind of scenario is comments. In a blog, for example, comments can go on a post or a page but maintain the same structure regardless if it’s a post or a page. This kind of behavior is described as polymorphism.

In this article, we will go over polymorphic relationships in Laravel, how they work and the various use cases in which they are best used.

What are polymorphic relationships in Laravel?

Taking the example mentioned above into consideration, we have two entities: Post and Page. To allow for comments on each of these, we can decide to set up our database like this:

posts:
  id
  title
  content
  
posts_comments:
  id
  post_id
  comment
  date
  
pages:
  id
  body
  
pages_comments:
  id
  page_id
  comment
  date

This approach above has us creating multiple comment tables — posts_comments and pages_comments — that do the same thing except that they are pointing to different entities.

With polymorphic relationships, we can follow a cleaner and simpler approach for the same situation.

posts:
  id
  title
  content
  
pages:
  id
  body
  
comments:
  id
  commentable_id
  commentable_type
  date
  body

By definition, polymorphism is the condition of occurring in several different forms, and this is the approach we are trying to follow above. We have two important new columns to take note of: commentable_id and commentable_type.

In the example above, we have merged page_comments and post_comments together by replacing both post_id and page_id in each table with commentable_id and commentable_type to derive the comments table.

The commentable_id column would contain the ID of the post or page, and commentable_type would contain the class name of the model that owns the record. The commentable_type would store something like App\Post, which is how the ORM will determine which model it belongs to and return when trying to access the value.

We made a custom demo for .
No really. Click here to check it out.

Here, we have three entities: PostPage, and Comments.

Post can have Comments.
Page can have Comments.
And Comments can belong to either Post or Page.

Let’s create our migrations:

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->text('content');
});

Schema::create('pages', function (Blueprint $table) {
    $table->increments('id');
    $table->text('body');
});

Schema::create('comments', function (Blueprint $table) {
    $table->increments('id');
    $table->morphs(‘commentable’);
    $table->text('body');
    $table->date('date');
});

$table→morphs('commentable') would automatically create two columns for the id and type using the text passed to it, so it will result in commentable_id and commentable_type.

Next, we create models for our entities:

//file: app/Post.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}


//file: app/Page.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Page extends Model
{
    /**
     * Get all of the page's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

//file: app/Comment.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get all of the models that own comments.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

In the code above, we have our models declared and also make use of two methods, morphMany() and morphTo, that help us define a one-to-many polymorphic relationship.

Both the Page and Post models have a comments() function that returns a morphMany() to the Comment model. This indicates that both of them are expected to have a relationship with many comments.

The Comment model has a commentable() function that returns a morphTo() function, indicating that this class is related to other models.

Once these are set up, it becomes easy to access data and work with this relationship through our models.

Here are some examples:

To access all of the comments for a page, we can use the comments dynamic property declared in the model.

// getting comments for a sample page...
  $page = Page::find(3);

  foreach($page->comment as $comment)
  {
    // working with comment here...
  }

For retrieving comments on a post:

// getting comments for a sample post...
  $post = Post::find(13);

  foreach($post->comment as $comment)
  {
    // working with comment here...
  }

You can also reverse this retrieval. In a situation where you have a comment ID and would like to find out which entity it belongs to using the commentable method on the Comment model, you can use:

$comment = Comment::find(23);

  // getting the model...
  var_dump($comment->commentable);

With this set up, you should know that the number of models that use the Comment relationship are not limited to two. You can have as many as possible added to this without any major changes or breaking code. For example, let’s create a new Product model added to your site that can also have comments.

First, we create the migration for the new model:

Schema::create('products', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
});

Then we create the model class:

//file: app/Product.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    /**
     * Get all of the product's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

And that’s it. Your comment system now supports your products and can be retrieved the same way as other entities.

// getting comments for a sample product...
  $product = Product::find(3);

  foreach($product->comment as $comment)
  {
    // working with comment here...
  }

Types of polymorphic relationships in Laravel

The example above has covered one-to-many polymorphic relationships where one model, e.g., a page, can have many models, e.g., comments, related to it. Similar to how relationships in relational databases work, there are other types of polymorphic relationships.

A one-to-one polymorphic relationship

A one-to-one polymorphic relationship is a situation where one model can belong to more than one type of model but on only one association. A typical example of this is featured images on a post and an avatar for a user. The only thing that changes however is how we get the associated model by using morphOne instead.

class Post extends Model
{
    /**
     * Get the post's only image.
     */
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

class User extends Model
{
    /**
     * Get the user's only image.
     */
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

A one-of-many polymorphic relationship

A one-of-many relationship is a situation where one model can have multiple associations with more than one model, but you only want to retrieve one at a time. This takes advantage of Laravel’s ofMany helper methods along with morphOne to retrieve the desired single association.

public function latestImage()
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

Check out Laravel’s documentation of the various ofMany methods that can be used to construct such relationships.

A many-to-many polymorphic relationship

Many-to-many polymorphic relationships handle slightly more complicated situations than one-to-many and one-to-one relationships. An easy example is when we have posts, pages, and tags. Multiple posts can have multiple tags. Multiple tags can belong to multiple pages.

In our post model, we can retrieve associated tags using morphToMany.

class Post extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}
And from the Tag model, return all connected associations from the desired model

class Tag extends Model
{
    /**
     * Get all of the posts that are assigned this tag.
     */
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
}

Additional use cases for polymorphic relationships

Multiple user types

A common use case where polymorphic relationships also come into play is when there is a need for multiple user types. These user types usually have some similar fields and then other fields unique to them. This could be a User and Admin type, a Driver or Rider type in the case of ride-sharing apps, or even applications where there are numerous kinds of users or professionals.

Every user may have a name, email, avatar phone, etc., before having additional details added to them. Here’s an example schema for a hiring platform that allows you to hire different kinds of artisans:

user:
   id
   name
   email
   avatar
   address
   phone
   experience
   userable_id
   userable_type
   
drivers:
  id
  region
  car_type //manual or automatic
  long_distance_drive
  
cleaners:
  id
  use_chemicals //uses chemicals in cleaning
  preferred_size //size of cleaning
  
  ...

In this scenario, we can get our users’ basic data without worrying about whether they are cleaners or not, andm at the same time, we can get their type from the userable_type and the ID from that table in the userable_id column when it is needed. This will be a one-to-one polymorphic relationship.

Using polymorphic relationships for attachments and media

In a similar scenario like the comment example provided above, posts and pages — and even messages — can require attachments or any other form of media. This works better than creating a table for every kind of attachment.

messages:
  id
  user_id
  recipient_id
  content
  
attachment:
  id
  url
  attachable_id
  attachable_type

In the above example, attachable_type can then be the model for messages, posts, or pages.

The general concept behind using polymorphic relationships revolves around identifying similarities between what two or more models might need and building on top of it instead of duplicating and creating numerous tables and code.

Conclusion

We have discussed the basic application of polymorphic relationships and its possible use cases. We should also note that polymorphic relationships are not a complete solution to everything and should only be used when convenient or feels like the right way to go. Is there an exciting use case of polymorphic relationships in your application? Share below!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Adewale Abati Web engineer, tech lifestyle YouTuber, public speaker. Building communities and open source for the Next Billion Users.

12 Replies to “Polymorphic relationships in Laravel and their use cases”

  1. Thanks for the great explanation. It was very easy to learn this content here.

    I have 2 questions:

    1. Are you missing S on $page->comment(s)? And in other loops too?
    foreach($page->comment as $comment)
    {
    // working with comment here…
    }

    2. In which column the comments are stored in comments table? Because we have only: Id, commendable_id, commendable_type and date.

  2. 2. Its an error in this article – in comments migrations we saw $table->date(‘body’); .. then must by $table->string(‘body’); or $table->text(‘body’); – body column is for the coment content 🙂

  3. Great article. I have one question: how would you return the inverse? Eg all comments of class Page?

  4. You guys have done a far better than job than the official Laravel docs. Cheers!

  5. This was exactly what I was looking for. All my scenarios were discussed here. This is fantastic. Thank you very much.

  6. Hi, greate article!!!
    Just one small mistake: it should be $table→morphs(‘commentable’) not $table→morphs(‘comment’) which would automatically create two columns using the text passed to (it won’t add able, atleast not in L8). So it will result in commentable_id and commentable_type.

Leave a Reply