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.
| Relationship | Plain-English meaning | Example |
|---|---|---|
| Many-to-many | Both sides can have many of the other | A post has many tags; a tag is on many posts |
| Has-many-through | Reach a distant table via a middle one | A country has many posts, through its users |
| Polymorphic | One relation that fits several models | One 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:
// 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:
$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 threeGoing 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:
// 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).
// 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?
✍️ Practice
- Build a many-to-many between posts and tags with
belongsToManyand useattach/sync. - Add a polymorphic
commentstable shared by two models.
🏠 Homework
- Model a blog where posts and photos both have comments (polymorphic) and posts have many tags (many-to-many). Seed some data.