Polymorphic relationships in Laravel and examples

Admin   Laravel   2307  2020-08-18 16:00:56

Quite often, software development uses models that can refer to several entities at the same time. This type of model has a universal structure that does not change for any specific model with which it is associated.

A common example of such an example is a comment. In a blog, for example, comments can be added to a specific post or page. However, the structure of the comment remains the same whether it is a post or a page.

In this article, we'll take a look at polymorphic relationships in Laravel, how they work, and various practical uses for them.

In this article, we'll take a look at polymorphic relationships in Laravel, how they work, and various practical uses for them.

What are polymorphic relationships in Laravel?

Considering the above example, we have two entities: Post and Page. To be able to add comments to each of them, we can build the database structure like this:

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

 

With this approach, we create multiple comment tables - posts_comments and pages_comments, which do the same thing, except that a separate table is created for each comment entity. Although, if you look closely, you can see that the comment tables repeat each other in their structure.

And therefore, let's try to fix it somehow, and make the repetitive logic. With polymorphic links, we can follow a cleaner and simpler approach in the same situation.

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



By definition, polymorphism is a state in which one function, in this case, an entity, can process data of different types. And this is the approach we are trying to follow above. We have two new important columns: commentable_id and commentable_type.

In the above example, we merged the page_comments and post_comments tables, replacing the post_id and page_id columns with the generic commentable_id and commentable_type columns to get the comment table.

The commentable_id column will contain the id of the post or page. And the commentable_type will contain the class name of the model to which the entry belongs. The commentable_type will store something like App\Entity\Post, so the ORM will determine which model the comment belongs to and return the desired entity when it is accessed.

Here we have three entities: Post, Page and Comments.

Post entity can have Comments

The Page entity can also have Comments

Comments can belong to both Post and Page entities.

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->text('body');
    $table->date('date');
});


The code $table->morphs('comment') will automatically create two columns using the text + "able" passed to it. So this will cast to commentable_id and commentable_type.

Next, we create model classes for our entities:

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

namespace App\Entity;

use Illuminate\Database\Eloquent\Model;

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


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

namespace App\Entity;

use Illuminate\Database\Eloquent\Model;

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


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

namespace App\Entity;

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 above code, we have declared our models and also use two methods, morphMany() and morphTo(), which help us create a polymorphic relationship between entities.

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

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

After all the tweaks, it becomes quite easy to access and work with the data through our models.

Here are some examples:

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

//
$page = Page::find(3);

foreach($page->comments as $comment){
    //
}

To get comments on a post:
 

$post = Post::find(13);

foreach($post->comments as $comment){
    //
}

Similarly, you can also reverse lookup the entity to which the comment belongs. In a situation where you have a comment ID and want to know which entity it belongs to, use the commentable method on the Comment model:

$comment = Comment::find(23);

var_dump($comment->commentable);

With all these settings, you should be aware that the number of models that use the comments relationship is not limited to two. You can add as many as possible without any major changes or code breaking. For example, let's create a new product model added to your site, which can also have comments.

First, we'll create a migration for the new model:

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

Then we create an entity class:

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

namespace App\Entity;

use Illuminate\Database\Eloquent\Model;

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

And that's all. Comments are now available for the Product entity, working with them in the same way as when working with other entities.

$product = Product::find(3);

foreach($product->comments as $comment){
    //
}

Additional uses for polymorphic bonds

Several types of users

A common use case where polymorphic relationships also come into play is when there is a need for multiple types of users. These custom types usually have some similar fields and then others that are unique to them. It can be a type of user and administrator, a driver or passenger in the context of a transportation application, or even applications where there are numerous types of users or professions.

Each user can have a name, e-mail, avatar phone, etc., as well as additional information fields. Here is an example of a schema for a platform that allows you to hire different workers:

 

user:
   id
   name
   email
   avatar
   address
   phone
   experience
   userable_id
   userable_type
   
drivers:
  id
  region
  car_type //manual || automatic
  long_distance_drive
 
cleaners:
  id
  use_chemicals
  preferred_size
  ...



In this scenario, we can get the underlying data of our users without worrying about whether they are cleaners or not, and at the same time, we can get their type from userable_type and id from this table in the userable_id column when needed.

Attachments and media

In a similar scenario as in the comment example above, posts, pages, and even posts may have attachments attached. In this case, polymorphic relationships work fine, instead of creating a separate table for each type of attachment.

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



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

The general concept of using polymorphic relationships solves the problem of determining the similarity between what two or more models might need, and builds on that instead of duplicating and creating numerous tables and code.

Summary

We discussed the main uses of polymorphic bonds and their possible uses. It should also be noted that polymorphic relationships are not a silver bullet for everything and should only be used when it is convenient or seems right.