Monday, 15 October, 2018 UTC


Summary

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.
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
Taking the example mentioned above into consideration, we have two entities – Post and Page. To have 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
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.
Here we have 3 entities – Post, Page 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(‘comment’);
    $table->date('body');
});
$table→morphs(``'``comment``'``) would automatically create two columns using the text passed to it + “able”. 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 the polymorphic relationship.
Both the Page and Post model have a comments() function that returns a morphMany() to the Comment Model. This indicates that both of them are expected to have 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 all these is set up, it becomes very easy to access data and work with this relationship through our models.
Here are some examples:
To access all 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:
$comment = Comment::find(23);

  // getting the model...
  var_dump($comment->commentable);
With all these 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...
  }
Additional use cases for polymorphic relationships
Multiple user types
A common use case where polymorphic relationships also come in to play is when there is 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 name, email, avatar phone etc before having additional details added to them. Here’s an example schema for a hiring platform that allows you 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 and 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.
Attachments and media
In a similar scenario like the comment example provided above, post 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, post or pages.
The general concept behind using polymorphic relationships resolves around identifying similarities between what 2 or more models might need and build on top of it instead of duplicating and creating numerous tables and code.
Conclusion
We have discussed the basic application of polymorphic relationships and the possible use cases that it can be taken advantage of. 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?

Plug: LogRocket, a DVR for web apps

LogRocket is a frontend logging tool 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.
Try it for free.