Polymorphic relationships in Laravel

   
Video thumbnail

When I started working with Laravel and Eloquent, one of the concepts that caught my attention the most was polymorphic relationships. They are extremely useful when we want to reuse the same table or model to associate it with multiple entities without duplicating structures.

In this guide, I explain not only how to handle error pages in Laravel, but also what polymorphic relationships are, how to use them, and I share real-world use cases—including working examples from my own project with books, payments, and files, as well as with publications.

In Laravel, to use many-to-many relationships, we have it very easy. Instead of creating a pivot table for each relationship as shown previously, we can create polymorphic relationships, which in other words, allows the use of the same pivot table for any relationship we want to relate with the tags. Laravel internally knows what each relationship belongs to by means of a tag:

Type field to identify the relationship

In this way, with this column that is internally managed by Laravel, we can use the same pivot table to map different types such as users, videos, or posts in our Laravel models transparently for us.

What are Polymorphic Relationships in Laravel?

A polymorphic relationship allows a model to be associated with several other models using a single flexible relationship.

For example: an `images` table can be linked to both `users` and `posts`.

Instead of creating a table for each relationship, Laravel uses generic columns like `imageable_id` and `imageable_type` to store the ID and the model it belongs to.

// Ejemplo simple
class Image extends Model {
   public function imageable(): MorphTo {
       return $this->morphTo();
   }
}

⚙️ Types of Polymorphic Relationships

Laravel offers various methods depending on the type of relationship you need:

Type    Method    Classic equivalent
One-to-one    morphOne()    hasOne()
One-to-many    morphMany()    hasMany()
Many-to-many    morphToMany() / morphedByMany()    belongsToMany()

For all types of polymorphic relationships, the `morph` prefix is used to define them:

  •     morphOne(MODEL, PIVOTTABLE): Defines a one-to-one polymorphic relationship. For example, a profile relationship can be used for different types like users, people, or companies; its equivalent to classic relationships is the hasOne() type.
  •  morphMany(MODEL, PIVOTTABLE): Used to define a one-to-many polymorphic relationship. For example, a categories relationship can be related to other models like posts or videos; its equivalent to classic relationships is hasMany().
  •     ManyToMany:           

    •             morphToMany(MODEL, PIVOTTABLE): This method is used to define a many-to-many polymorphic relationship. For example, the tags table which can be related to different types of models like posts or videos.        

           

    •             morphedByMany(MODEL, PIVOTTABLE): This method is used in a model to establish a many-to-many relationship with other models using a polymorphic relationship. This relationship is placed on the "taggable" model.        

       

With this structure, the same model can be shared by multiple entities without duplicating code.

Another example of a relationship that can be polymorphic is documents/comments, which can belong to a person, post, user, among others. This type of relationship would be a one-to-many polymorphic type.

Previously, we used a polymorphic relationship between users and authentication tokens via Sanctum.

Polymorphic relationships allow a record in one table to be related to multiple different models.

Polymorphic relationships in Laravel Eloquent are a powerful tool for handling situations where a record can be related to different entities. Instead of creating separate tables for each type of relationship, polymorphic relationships allow us to establish flexible connections between models. Here is an introduction with examples:

Unlike traditional relationships (like 1 to n or n to n), where the relationship is always fixed, in polymorphic relationships, the relationship can vary depending on the record.

Although we started by introducing the use of polymorphism for many-to-many relationships, we can also use them in the rest of the relationships, but it is in the use of many-to-many relationships where it is most important (and also one-to-many).

It is important to note that for the main relationship, the tags, the morphToMany() method is used to define the relationship, and for the "taggable" model, morphedByMany() is used. That is, the latter would be the one that has the polymorphic relationship. 

The definition of these methods can be a bit abstract, so let's look at some examples.

Example 1: Many-to-Many Relationship posts and tags

In this exercise, we are going to create the relationships between tags and posts, where a post can have 0 to N tags and a tag can be assigned to 0 or N posts.

We start by creating the migrations:

$ php artisan make:migration create_tags_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTagsTable extends Migration
{
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('tags');
    }
}

Another migration for the pivot table:

$ php artisan make:migration create_taggables_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTaggablesTable extends Migration
{
    public function up()
    {
        Schema::create('taggables', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('tag_id');             $table->unsignedBigInteger('taggable_id');
            $table->string('taggable_type'); // 'App\Models\Post'
            $table->timestamps();
            
            $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
        });
    }
    public function down()
    {
        Schema::dropIfExists('taggables');
    }
} 

Which usually has the suffix of able as in the previous case.

And in the models:

namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

In the posts controller, we can make some modifications for assigning tags to posts:

class PostController extends Controller
{
    public function create()
    {
        $tags = Tag::pluck('id', 'title');
        $categories = Category::pluck('id', 'title');
        $post = new Post();
        return view('dashboard.post.create', compact('post', 'categories''tags'));
    }
    public function store(StorePostPost $request)
    {
        $post = Post::create($requestData);
        $post->tags()->sync($request->tags_id);
        *
    }
    
    public function edit(Post $post)
    {
        $tags = Tag::pluck('id', 'title');
        *
        return view('dashboard.post.edit', compact('post', 'categories''tags'));
    }
    public function update(UpdatePostPut $request, Post $post)
    {
        //$post->tags()->attach(1);
        $post->tags()->sync($request->tags_id);
        *
    }
}

As for the view, it looks like this:

<div class="mt-3">
    <label for="tag_id">Tags</label>
    <select multiple class="form-control" name="tags_id[]" id="tags_id">
        @foreach ($tags as $title => $id)
    <option {{ in_array($id, old('tags_id') ?: $post->tags->pluck("id")->toArray()) ? "selected": "" }} value="{{ $id }}">{{ $title }}</option>
        @endforeach
    </select>
</div>

With the code above, we create a multiple selection list of all tags; the tags that are assigned to the post are selected by default.

We also create the CRUD process for tags:

<?php
namespace App\Http\Controllers\Dashboard;
use App\Models\Tag;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tag\PutRequest;
use App\Http\Requests\Tag\StoreRequest;
class TagController extends Controller
{
    public function index()
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.index')) {
            return abort(403);
        }
        $tags = Tag::paginate(2);
        return view('dashboard/tag/index', compact('tags'));
    }
    public function create()
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.create')) {
            return abort(403);
        }
        $tag = new Tag();
        return view('dashboard.tag.create', compact('tag'));
    }
    public function store(StoreRequest $request)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.create')) {
            return abort(403);
        }
        Tag::create($request->validated());
        return to_route('tag.index')->with('status', 'Tag created');
    }
    public function show(Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.index')) {
            return abort(403);
        }
        return view('dashboard/tag/show', ['tag' => $tag]);
    }
    public function edit(Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.update')) {
            return abort(403);
        }
        return view('dashboard.tag.edit', compact('tag'));
    }
    public function update(PutRequest $request, Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.update')) {
            return abort(403);
        }
        $tag->update($request->validated());
        return to_route('tag.index')->with('status', 'Tag updated');
    }
    public function destroy(Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.destroy')) {
            return abort(403);
        }
        $tag->delete();
        return to_route('tag.index')->with('status', 'Tag delete');
    }
}

In the code above, only the controller is shown. You must implement the rest of the code such as the Requests classes, views, routes, and associated permissions. If you have any questions, you can check the source code at the end of the section.

Now, we will look at some other examples that were taken from the official documentation and that you can use as a reference to know how to use the rest of the available relationship types. If you could understand the many-to-many polymorphic relationship we presented before, these will be much easier to understand.

Example 2: One-to-Many Relationship

In this example, our main model is comments. As "able/selectable" type models, we have videos and posts; that is, comments can be used by the posts and videos entities:

posts
    id - integer
    title - string
    body - text
 
videos
    id - integer
    title - string
    url - string
 
comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

As for the models, they look like this:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Comment extends Model
{
    /
     * Get the parent commentable model (post or video).
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Post extends Model
{
    /
     * Get all of the post's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Video extends Model
{
    /
     * Get all of the video's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Example 3: One-to-One Relationship

In this example, our main model is images. As "able/selectable" type models, we have users and posts; that is, these entities have an associated image.

posts
    id - integer
    name - string
 
users
    id - integer
    name - string
 
images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

The migration:

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

As for the models, they look like this:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Image extends Model
{
    /
     * Get the parent imageable model (user or post).
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class Post extends Model
{
    /
     * Get the post's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class User extends Model
{
    /
     * Get the user's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Example 4: One-to-Many Relationship books and payments, leftJoin and optional 1-N polymorphic relationships

Video thumbnail

In my personal project —an API for selling books that also handles courses and files— I took advantage of polymorphic relationships to unify the payment system (FilePayment).

Each purchase belongs to a different resource (book, course, etc.), but the payment structure is the same.
Instead of duplicating tables, I used a one-to-many polymorphic relationship:

class FilePayment extends Model {
   public function filePaymentable(): MorphTo {
       return $this->morphTo();
   }
}
class Book extends Model {
   public function filePayments() {
       return $this->morphMany(FilePayment::class, 'filePaymentable');
   }
}

Example 5: Inverse Many-to-Many Relationship in Laravel

Video thumbnail

It is often necessary to get the records of a principal entity given the secondary one in a many-to-many relationship; the typical scenario is that we have a many-to-many relationship between posts and tags, and we want to get the posts that belong to certain tags; for this, we can make a query like the following:

 Post::whereHas('tags', function($q) {
       $q->where('tag_id', 1);
    })->whereHas('tags', function($q) {
       $q->where('tag_id', 4);
    })->get();

Or if the tags are dynamic:

$tag_ids=[25,40,30];
    Post::where(function($query)use($tag_ids){
        foreach ($id as $value){
            $query->whereHas('tags',function ($query)use($value){
                $query->where('tag_id',$value);
            });
        }
    })->get();

The Post model looks like this:

class Post extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class, 'post_tags', 'post_id', 'tag_id');
    }
}

Validation with instanceof in Polymorphic Relationships

Video thumbnail

Another useful situation: validating the actual type of the associated model before operating on it. I want to show you a possible use of `instanceof` in Laravel's polymorphic relationships.

For example, when downloading a file, I want to make sure it truly belongs to a book:

if ($file->fileable instanceof Book) {
   // Ejecutar lógica de descarga segura
}

The model is this:

class Book extends Model
{
  // ...
  public function files()
  {
      return $this->morphMany(File::class, 'fileable');
  }
}

These validations prevent errors or possible vulnerabilities if a user tries to access a resource that doesn't belong to them.

A book can have zero to many files, as observed.

Therefore, it is a one-to-many relationship between books and files.

And those files can belong to different types of models: books, images, videos, etc.

In the future, files could be associated with videos, images, or other models, so we want to avoid any possible problem —either due to a logic error or improper manipulation by the user—.

class File extends Model
{
  // ...
  protected $fillable = ['file', 'type', 'fileable_type', 'fileable_id'];
  public function fileable(): MorphTo
  {
      return $this->morphTo();
  }
}

In summary, the use of `instanceof` within polymorphic relationships allows us to ensure that the object we are processing corresponds to the expected model type.

In this way, when processing a file related to a book, we can be certain that it truly belongs to a Book, preventing errors in subsequent processes or vulnerabilities within the application.

Best Practices and Common Mistakes

✅ Best Practices:

  • Use consistent names (*_able) in polymorphic columns.
  • Place relationship filters inside the join closure.
  • Validate types with `instanceof` when working with dynamic resources.
  • Clearly document which models can use the relationship.

❌ Common Mistakes:

  • Using `join` instead of `leftJoin` when you need all records.
  • Duplicating pivot tables instead of reusing them polymorphically.
  • Not correctly defining the types in migrations (`morphs()` solves this).

Conclusion

Polymorphic relationships in Laravel are a powerful tool that saves you time and code when handling reusable models.

Mastering this pattern allows you to create clean, scalable, and easy-to-maintain architectures.

In summary, in the last two examples, we see that we have to use the morphTo() method for the main relationship, and for the "able/selectable" types, the morphMany() and morphOne() methods are used respectively.

Finally, the morph() type methods for specifying relationships receive several parameters that can be useful if Laravel fails to correctly interpret the table and relationship name, for example:

return $this->morphToMany(Tag::class, 'taggable', 'taggables', 'taggable_id');

As an additional consideration, as we mentioned earlier, many-to-many and one-to-many polymorphic relationships are the most interesting in this type of relationship because, when wanting to make a many-to-many relationship "taggable/able" without being polymorphic, we would have to duplicate the pivot table for that purpose (and in the one-to-many, we couldn't create it to simulate a polymorphic type of the same kind). However, with the use of polymorphic relationships, we can use the same table; conversely, the use of polymorphic relationships for the one-to-one type can be easily managed through a traditional relationship of the same type, meaning one that is not polymorphic. For example, let's remember that for one-to-one relationships we have:

posts
    id - integer
    name - string
 
users
    id - integer
    name - string
 
images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

We could have a similar relationship to the previous one where posts and users can have an image through:

posts
    id - integer
    name - string
    image_id - integer
 
users
    id - integer
    name - string
    image_id - integer
 
images
    id - integer
    url - string

More information about relationships at:

https://laravel.com/docs/master/eloquent-relationships

❓ Frequently Asked Questions

When is it convenient to use polymorphic relationships?
When a model (like Image or FilePayment) can be related to multiple entities without repeating structure.

What is the difference between morphToMany and morphedByMany?
`morphToMany` is used in the model that has the relationship, while `morphedByMany` is used in the model that "receives" that relationship.

Can I use morphs() in migrations for all cases?
Yes, `morphs('name')` automatically creates the `name_id` and `name_type` columns.

The next step is to learn how to handle the N+1 problem in Laravel, which is very prone to many-to-one/many relationships.

I agree to receive announcements of interest about this Blog.

We will see what polymorphic type relationships are and how to use them in Laravel, including one-to-one, one-to-many, and many-to-many relationships.

| 👤 Andrés Cruz

🇪🇸 En español