Skip to main content
Create your own
Lesson illustration

Polymorphic Associations: Flexible Data Structures

Hello! Let's continue our journey into advanced Eloquent features.

Introduction

In our last lesson, we learned how to encapsulate query logic using local and global scopes, which made our code cleaner and more readable. We focused on adding reusable constraints to our queries.

Today, we shift from constraining queries to designing more flexible data structures. We'll tackle a common challenge in application development: what if you have a feature, like comments or tags, that needs to apply to different kinds of content? For example, users might want to comment on Posts, Videos, or Products. How do you handle that without creating separate post_comments, video_comments, and product_comments tables?

This lesson answers that question. Our goal is to define and query polymorphic relationships for flexible data structures. By the end of this lesson, you'll be able to design a database schema and write Eloquent code that allows one model to belong to several other model types, all using a single database table and association.

1. The "Why": The Problem Polymorphism Solves

Imagine your application has Post models and Product models. Now, a new requirement comes in: users should be able to leave comments on both posts and products.

The naive approach would be to create two separate tables:

  • post_comments with a post_id foreign key.
  • product_comments with a product_id foreign key.

This works, but it's not scalable. What happens when you add Video models and they also need comments? You'd have to create a third video_comments table. This leads to code duplication and a cluttered database schema.

Polymorphic relationships solve this by allowing a model (e.g., Comment) to belong to more than one type of parent model (Post, Product, Video) using a single, clever association.

Laravel Advanced - Polymorphic Relationship

To understand the core problem and the polymorphic solution, watch the beginning of this video from Laratips. The creator uses a simple spreadsheet analogy to clearly explain why you'd choose a polymorphic structure over creating many redundant tables.

Watch from the start until 04:55. Focus on the two approaches presented for adding comments to posts and the scalability argument for the polymorphic approach.

The key idea is to have a single comments table that includes two special columns:

  • commentable_id: Stores the ID of the parent model (e.g., the ID of the Post or Product).
  • commentable_type: Stores the class name of the parent model (e.g., App\Models\Post or App\Models\Product).

Together, these two columns uniquely identify the parent of any given comment. Here is a visual representation of how comments and tags can relate to both videos and posts.

Laravel Polymorphic Relationships Database Schema
This ERD illustrates the database schema for polymorphic relationships. Notice the `commentable_id` and `commentable_type` columns in the `comments` table, and the `taggable_id` and `taggable_type` in the `taggables` pivot table, which allow them to connect to multiple parent tables like `posts` and `videos`.

2. One-to-Many Polymorphic Relationships

This is the most common type of polymorphic relationship. A parent can have many "children," and these children can belong to different types of parents. Our Post/Video/Comment example is a perfect one-to-many polymorphic relationship.

Let's walk through how to set this up.

Database Migration

In your comments table migration, instead of adding a standard foreignId, you use the morphs() helper method.

// In database/migrations/xxxx_xx_xx_xxxxxx_create_comments_table.php

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    // This single line creates two columns:
    // `commentable_id` (an unsigned big integer)
    // `commentable_type` (a string)
    // It also adds an index on these columns.
    $table->morphs('commentable'); 
    $table->timestamps();
});

The name commentable is derived from the child model (Comment) and the suffix -able. This is a Laravel convention.

Model Relationships

Next, you define the relationships in your Eloquent models.

  1. Parent Models (Post, Product): The parent models use the morphMany() method to declare that they can have many comments.

    // In app/Models/Post.php
    public function comments()
    {
        // The first argument is the related model's class.
        // The second argument is the relationship name ('commentable').
        return $this->morphMany(Comment::class, 'commentable');
    }
    
  2. Child Model (Comment): The child model uses the morphTo() method to define the inverse relationship. This tells Eloquent that it can belong to any model.

    // In app/Models/Comment.php
    public function commentable()
    {
        // The method name should match the prefix used in the migration.
        return $this->morphTo();
    }
    

Eloquent: Relationships - Polymorphic Relationships

For a definitive guide on the model structure, please review the official Laravel documentation. It provides clear code examples for the morphMany and morphTo relationship definitions.

Read the subsections "Table Structure" and "Model Structure" under the "One to Many (Polymorphic)" heading. This will reinforce the migration and model setup we just discussed.

3. Working with One-to-Many Polymorphic Data

Now that the relationships are defined, you can interact with them just like any other Eloquent relationship.

Laravel Advanced - Polymorphic Relationship

Let's return to the Laratips video to see a detailed practical demonstration. The presenter uses Tinkerwell to create, retrieve, update, and delete comments for different parent models.

Watch from 04:55 to 28:46. This is a longer segment, but it's packed with practical, hands-on examples. Pay close attention to: The migration setup with morphs() (04:55 - 06:38). Defining morphMany and morphTo in the models (06:38 - 10:53). Creating comments for a post: $post->comments()->create(...) (11:24). Retrieving a comment's parent: $comment->commentable (22:40). Eager loading comments with a parent model (26:39).

Here's a summary of the key operations:

  • Creating a comment for a post:

    $post = Post::find(1);
    $post->comments()->create(['body' => 'This is a great post!']);
    

    Laravel automatically sets commentable_id to 1 and commentable_type to App\Models\Post.

  • Retrieving comments from a post:

    $post = Post::find(1);
    $comments = $post->comments; // Returns a collection of Comment models
    
  • Finding the parent of a comment:

    $comment = Comment::find(5);
    $parent = $comment->commentable; // Returns either a Post or Product instance
    

4. Many-to-Many Polymorphic Relationships

This relationship is slightly more complex. It's used when a model can have many instances of another model, and that other model can also belong to many different parent types. The classic example is tags.

  • A Post can have many Tags.
  • A Video can have many Tags.
  • A Tag (e.g., 'Laravel') can be applied to many Posts and many Videos.

This requires a pivot table, just like a standard many-to-many relationship. The pivot table (e.g., taggables) will contain the polymorphic columns.

  • tag_id: Foreign key for the tags table.
  • taggable_id: The ID of the parent model (Post or Video).
  • taggable_type: The class name of the parent model.

Model Relationships

  1. Parent Models (Post, Video): Use the morphToMany() method.

    // In app/Models/Post.php
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
    
  2. Child Model (Tag): On the Tag model, you must define the inverse for each possible parent, using morphedByMany().

    // In app/Models/Tag.php
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
    
    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
    

Laravel 6 Advanced - e3 - Polymorphic Relationships

This video from Coder's Tape provides a clear explanation and demonstration of the many-to-many polymorphic relationship, which complements the previous video.

Watch from 14:40 to 21:09. The video explains the concept of tags and walks through setting up the taggables pivot table and the morphToMany/morphedByMany relationships.

5. Querying Polymorphic Relationships & Optimization

A key part of your goal is mastering query optimization. Polymorphic relationships, if not handled carefully, can lead to performance issues like the N+1 query problem.

Imagine fetching all comments and displaying their parent's title:

$comments = Comment::all();

foreach ($comments as $comment) {
    // This line runs one query for EACH comment!
    echo $comment->commentable->title; 
}

If you have 100 comments, this loop will execute 101 queries (1 for all comments, and 100 more for each parent).

Eager Loading to the Rescue

You solve this using eager loading with the with() method, just as you would for regular relationships.

// Runs only 3 queries, regardless of the number of comments!
// 1. SELECT * FROM comments
// 2. SELECT * FROM posts WHERE id IN (...)
// 3. SELECT * FROM videos WHERE id IN (...)
$comments = Comment::with('commentable')->get();

foreach ($comments as $comment) {
    echo $comment->commentable->title; 
}
Laravel Lazy Loading vs Eager Loading Comparison
This infographic highlights the difference between Lazy Loading (N+1 problem) and Eager Loading (optimized). Eager loading with `with()` is crucial for performant polymorphic queries.

Advanced Filtering: whereHasMorph

What if you want to retrieve only the comments that belong to posts with a title starting with "Laravel"? You can use the whereHasMorph method.

Eloquent: Relationships - Querying Morph To Relationships

The official documentation provides the best explanation for advanced querying techniques like whereHasMorph.

Read the section "Querying Morph To Relationships". Pay attention to the syntax of whereHasMorph and how you can use a wildcard * to query against all possible parent types.

Here's an example based on the documentation:

use App\Models\Comment;
use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;

// Retrieve comments for Posts where the post's title starts with 'Laravel'.
$comments = Comment::whereHasMorph(
    'commentable',
    Post::class,
    function (Builder $query) {
        $query->where('title', 'like', 'Laravel%');
    }
)->get();

6. Best Practice: Custom Polymorphic Types

By default, Laravel stores the full class name (e.g., App\Models\Post) in the ...able_type column. This couples your database to your application's file structure. If you ever refactor and move the Post model, your existing polymorphic data will break.

A better approach is to define a "morph map". This lets you use a simple, stable alias (like 'post') in the database.

You define this map in the boot() method of your App\Providers\AppServiceProvider.

// In app/Providers/AppServiceProvider.php

use Illuminate\Database\Eloquent\Relations\Relation;

public function boot(): void
{
    Relation::enforceMorphMap([
        'post' => 'App\Models\Post',
        'video' => 'App\Models\Video',
        'product' => 'App\Models\Product',
    ]);
}

Now, when you create a comment for a post, Laravel will store 'post' in the commentable_type column instead of the full class name, making your application much more robust and maintainable.

Conclusion

You've learned how to design and use one of Eloquent's most powerful features for creating flexible and scalable applications.

Key Takeaways:

  • Polymorphic relationships allow a model to belong to more than one type of parent model using a single association, avoiding table duplication.
  • One-to-many (morphMany/morphTo) is used for things like comments on posts/videos.
  • Many-to-many (morphToMany/morphedByMany) is used for things like tags on posts/videos and requires a polymorphic pivot table.
  • The key database columns are [relationship_name]_id and [relationship_name]_type.
  • Always eager load (with('...able')) polymorphic relationships to avoid the N+1 query problem.
  • Use whereHasMorph for advanced filtering based on parent model attributes.
  • Use a morph map (Relation::enforceMorphMap) as a best practice to decouple your database from your application structure.

Next Up:

We've just scratched the surface of advanced relationship queries with whereHasMorph. In the next lesson, we'll dive deeper into this topic by exploring subqueries and other advanced relationship query methods like whereHas and withCount, giving you even more power to fetch complex data efficiently.

Can't find a good explanation? Sign up and we'll make it for you

Sign up