Database & EloquentPro· 45 min read

Relationships in Depth (Many-to-Many & Polymorphic)

Go beyond one-to-many — model tags shared across posts, and one comments table that works for many models.

What you will learn

  • Build a many-to-many relationship with a pivot table
  • Use has-many-through to reach distant data
  • Understand polymorphic relationships

Three relationships every real app needs

You already know hasMany / belongsTo (a post has many comments). Real apps need three more shapes, and Eloquent makes each one short to declare. We will define them in plain words first, then in code.

RelationshipPlain-English meaningExample
Many-to-manyBoth sides can have many of the otherA post has many tags; a tag is on many posts
Has-many-throughReach a distant table via a middle oneA country has many posts, through its users
PolymorphicOne relation that fits several modelsOne comments table for both posts and videos

Many-to-many — the pivot table

When both sides can have many of the other, neither table can hold the link. Instead a third pivot table sits in the middle. For posts and tags it is called post_tag (both names, singular, alphabetical) and holds just two columns: post_id and tag_id. Each row says "this post wears this tag".

You declare the relationship with belongsToMany on both models:

A post belongs to many tags, and vice versa
// In the Post model
public function tags() {
    return $this->belongsToMany(Tag::class);
}

// In the Tag model
public function posts() {
    return $this->belongsToMany(Post::class);
}

Now you read and change the links with simple calls. attach adds a link, detach removes one, and the relationship reads like a property:

Attach, detach and sync tags through the pivot table
$post = Post::find(1);

$post->tags;              // all tags on this post (a collection)
$post->tags()->attach(3);  // add tag #3 to this post
$post->tags()->detach(3);  // remove tag #3
$post->tags()->sync([1, 2, 5]); // make the tags exactly these three

Going line by line: $post->tags returns every tag on post 1. attach(3) inserts a row (post_id 1, tag_id 3) into the pivot table. detach(3) deletes that row. sync([1, 2, 5]) is the clever one — it adjusts the pivot so the post ends up linked to exactly tags 1, 2 and 5, adding and removing as needed (perfect for a "tags" checkbox group on an edit form).

Note: Output: $post->tags → a collection of Tag models, e.g. [PHP, Laravel, Beginner]. After sync([1, 2, 5]) the post is linked to tags 1, 2 and 5 only — any other links are removed.

Has-many-through — reaching distant data

Sometimes the data you want is two tables away. A Country has many Users, and each user has many Posts. To get all posts written by people from one country, you would normally join through users. hasManyThrough does it in one declaration:

A country has many posts, through its users
// In the Country model
public function posts() {
    return $this->hasManyThrough(Post::class, User::class);
}

This reads as "give me all posts that belong to users that belong to this country". Then Country::find(1)->posts returns every post by anyone from country 1 — Eloquent writes the two-step join for you.

Polymorphic — one relation, many owners

Polymorphic (poly = many, morph = shape) means one relationship that can attach to different model types. Imagine you want comments on both posts and videos. Instead of two near-identical post_comments and video_comments tables, you keep one comments table with two special columns: commentable_id (the id of the owner) and commentable_type (which model it is — Post or Video).

One comments table serves both posts and videos
// In the Comment model
public function commentable() {
    return $this->morphTo();
}

// In BOTH the Post and Video models
public function comments() {
    return $this->morphMany(Comment::class, 'commentable');
}

On the Comment side, morphTo() means "my owner could be any model — read the type column to find out". On the Post and Video side, morphMany means "I have many comments". So $post->comments and $video->comments both work, pulling from the same table. $comment->commentable gives back whichever post or video the comment belongs to.

Note: Output: $post->comments → comments whose commentable_type is "App\Models\Post" and commentable_id is this post’s id. $comment->commentable → the actual Post (or Video) that owns the comment.

Tip: Add ->withTimestamps() to a belongsToMany to track when each pivot link was created, and store extra pivot columns (like a role) with ->withPivot('role').

Q. A post can have many tags and a tag can be on many posts. Which relationship is this?

Answer: When both sides can have many of the other, it is many-to-many — declared with belongsToMany and backed by a pivot table (e.g. post_tag).

✍️ Practice

  1. Build a many-to-many between posts and tags with belongsToMany and use attach/sync.
  2. Add a polymorphic comments table shared by two models.

🏠 Homework

  1. Model a blog where posts and photos both have comments (polymorphic) and posts have many tags (many-to-many). Seed some data.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →