Laravel Polymorphic Relationships: Easy Guide for Devs

Laravel Polymorphic Relationships: Easy Guide for Devs

If you’ve been working with Laravel Polymorphic Relationships for a while, you’ve probably hit that moment where you think — “Do I really need to create a separate post_comments table AND a video_comments table AND a product_comments table?” The answer is no. And that’s exactly where polymorphic relationships save you from a messy, repetitive database design.

Polymorphic relationships are one of those Eloquent features that most developers either skip over or find confusing the first time. But once it clicks, you’ll use it everywhere. This article breaks it down developer-to-developer — no fluff, just real code and real use cases.

What Is a Polymorphic Relationship and Why Do You Need It?

Let’s say your app has two models — Post and Video. Both of them can have comments. The naive approach is to create two separate tables:

  • post_comments
  • video_comments

Now imagine you add a Product model. You create a product_comments table. Then a Photo model — another table. You get the idea. It’s messy, it’s repetitive, and it violates the DRY principle.

A polymorphic relationship lets a single comments table belong to multiple models. Instead of duplicating your comment logic across tables, you store all comments in one place and use two special columns to track which model each comment belongs to.

Those two columns are:

  • commentable_id — the ID of the related model (e.g., 5 for Post ID 5)
  • commentable_type — the class name of the related model (e.g., App\Models\Post)

This is the core idea. One table. Multiple parent models. Clean, scalable, reusable.

One to One vs One to Many — What’s the Difference?

Before writing any code, let’s quickly clarify the two most common polymorphic types.

One to One Polymorphic means each related model can have one polymorphic record. For example, each Post and each User can have one featured Image.

One to Many Polymorphic means each related model can have many polymorphic records. For example, each Post and each Video can have many Comments.

In real-world apps, One to Many is by far the more common use case — so that’s what we’ll focus on most. But we’ll cover both.

Setting Up the Database Migrations

Let’s build a real example. We have Post and Video models, and we want both to support comments through a single comments table.

The Posts Migration

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

The Videos Migration

PHP
Schema::create('videos', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('url');
    $table->timestamps();
});

The Comments Migration — The Important One

PHP
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->unsignedBigInteger('commentable_id');   // ID of the related model
    $table->string('commentable_type');             // Class name of the related model
    $table->timestamps();
});

Or even cleaner — Laravel gives you a shortcut:

PHP
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable'); // Creates both commentable_id and commentable_type
    $table->timestamps();
});

morphs('commentable') is the magic helper. It creates both columns and even adds a composite index on them for better query performance.

Setting Up the Models

Now let’s wire up the Eloquent models.

The Comment Model

PHP
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $fillable = ['body'];

    /**
     * Get the parent commentable model (Post or Video).
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

The morphTo() method tells Laravel: “Look at the commentable_type and commentable_id columns to figure out which model this comment belongs to.”

The Post Model

PHP
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'body'];

    /**
     * Get all comments for this post.
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

The Video Model

PHP
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Video extends Model
{
    protected $fillable = ['title', 'url'];

    /**
     * Get all comments for this video.
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Both Post and Video use morphMany() — because each of them can have many comments. The second argument 'commentable' must match the prefix you used in your migration (commentable_id and commentable_type).

Saving Polymorphic Data

Now let’s actually create some comments.

Adding a Comment to a Post

PHP
$post = Post::find(1);

$post->comments()->create([
    'body' => 'This is a great post!',
]);

Behind the scenes, Laravel automatically sets commentable_id = 1 and commentable_type = 'App\Models\Post'. You don’t touch those columns manually.

Adding a Comment to a Video

PHP
$video = Video::find(3);

$video->comments()->create([
    'body' => 'Awesome video, thanks for sharing!',
]);

Same pattern, different parent model. Laravel handles the rest.

Retrieving Polymorphic Data

Get All Comments for a Post

PHP
$post = Post::with('comments')->find(1);

foreach ($post->comments as $comment) {
    echo $comment->body;
}

Get the Parent of a Comment (the morphTo side)

PHP
$comment = Comment::find(5);

$parent = $comment->commentable; // Returns a Post or Video instance

This is the beauty of it — $comment->commentable dynamically returns the right model based on the commentable_type stored in the database.

Eager Loading to Avoid N+1

Always eager load when looping:

PHP
$comments = Comment::with('commentable')->get();

foreach ($comments as $comment) {
    echo get_class($comment->commentable); // App\Models\Post or App\Models\Video
    echo $comment->body;
}

One to One Polymorphic — A Quick Example

Let’s say each Post and each User can have one featured Image.

Migration

PHP
Schema::create('images', function (Blueprint $table) {
    $table->id();
    $table->string('url');
    $table->morphs('imageable'); // imageable_id + imageable_type
    $table->timestamps();
});

Image Model

PHP
class Image extends Model
{
    public function imageable()
    {
        return $this->morphTo();
    }
}

Post Model

PHP
class Post extends Model
{
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

User Model

PHP
class User extends Model
{
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Usage

PHP
// Get the image for a post
$post = Post::find(1);
echo $post->image->url;

// Get the image for a user
$user = User::find(2);
echo $user->image->url;

// Get the parent of an image
$image = Image::find(1);
$parent = $image->imageable; // Returns Post or User

Clean and simple. One images table serves both models.

Custom Morph Map — Clean Up Those Class Names

By default, Laravel stores the full class name in commentable_type, like App\Models\Post. That’s fine, but it’s fragile — if you ever rename or move your model, all those stored values break.

A better approach is to register a morph map in your AppServiceProvider:

PHP
use Illuminate\Database\Eloquent\Relations\Relation;

public function boot()
{
    Relation::morphMap([
        'post'    => \App\Models\Post::class,
        'video'   => \App\Models\Video::class,
        'comment' => \App\Models\Comment::class,
    ]);
}

Now Laravel stores post instead of App\Models\Post in the database. It’s shorter, cleaner, and refactor-safe.

Real-World Recap — What Did We Build?

Here’s a quick visual of what the data looks like in the comments table:

idbodycommentable_idcommentable_type
1Great post!1App\Models\Post
2Loved this video3App\Models\Video
3Really helpful, thanks!1App\Models\Video
4Nice write-up2App\Models\Post

One table. Two parent models. No duplication. That’s polymorphic relationships in action.

When Should You Use Polymorphic Relationships?

Use them when:

  • Multiple models need the same kind of related data (comments, images, tags, likes, notifications)
  • You want to keep your database schema DRY and avoid redundant tables
  • The related model (like Comment) doesn’t fundamentally change based on its parent

Don’t use them when:

  • The related data has very different structures depending on the parent
  • You need complex joins and filtering — polymorphic queries are slightly harder to query at the raw SQL level
  • Your team is new to Laravel and the added abstraction causes confusion

Wrapping Up

Laravel Polymorphic Relationships aren’t complicated — they just look intimidating at first. Once you understand the morphs, morphMany, morphOne, and morphTo pattern, you’ll start seeing use cases everywhere: comments, tags, images, reactions, notifications, activity logs.

The key things to remember:

  • Use morphs('commentable') in your migration to create both columns
  • Use morphMany() or morphOne() in the parent model
  • Use morphTo() in the child model
  • Register a morph map in AppServiceProvider to keep class names clean
  • Always eager load with with('commentable') to avoid N+1 issues

That’s it. Go refactor those duplicate comment tables. 🚀


Have questions or a use case where you’re not sure if polymorphic is the right fit? Drop it in the comments below.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *