Skip to main content
Tutorial Free

Laravel Polymorphic Many-To-Many: Get All Related Records

January 21, 2023
3 min read

In Laravel's many-to-many polymorphic relations, there is a situation where you can't get ALL records of different models by their "parent" record. Let me explain, and show the potential solution.

Scenario: you have multiple Models that each may have multiple tags.

Example Tags: "eloquent", "vue", "livewire"

And then each Post, Video, and Course may have many tags.

Our Task: get all records (Posts + Videos + Courses) by a specific tag.

Unfortunately, there's nothing like $tag->taggables()->get(). You will see the solution for this below, but let's go step-by-step.

Here's the DB schema for this:

tags
id - integer
name - string
...
 
posts
id - integer
post_title - string
...
 
videos
id - integer
video_title - string
...
 
courses
id - integer
course_title - string
...
 
taggables
tag_id - `foreignId('tags')->constrained()`
taggable_id - integer (ID of post or video or course)
taggable_type - string (Model name, like "App\Models\Post")

The DB table taggables deserves its migration to be shown, it looks like this:

Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained();
$table->morphs('taggable');
});

Here's what the data in that DB table would look like:

Laravel polymorphic many-to-many taggables

Then, in the Eloquent Models, you have this code.

app/Models/Post.php

class Post extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}

Similarly, the Models of Course and Video will have the same identical tags() method with morphToMany().

And then, if needed, the Tag model has multiple morphedByMany() relations.

app/Models/Tag.php

class Tag extends Model
{
public function posts()
{
return $this->morphedByMany(Post::class, 'taggable');
}
 
public function videos()
{
return $this->morphedByMany(Video::class, 'taggable');
}
 
public function courses()
{
return $this->morphedByMany(Course::class, 'taggable');
}
}

Now, how to query data. How to get the entries by Tag?

Unfortunately, there's no way to run a single query, like $tag->taggables()->get();, because there's no single Model structure for different Post/Video/Course, they all have different fields, so how you can group them together?

Well, the trick is to run three different queries, but then combine the results into an identical structure and merge them together into one Collection. From there, you can paginate or transform that collection however you want.

$tag = Tag::find(1);
$posts = $tag->posts()->get()->map(fn($post) => [
'id' => $post->id,
'title' => $post->post_title
]);
 
$videos = $tag->videos()->get()->map(fn($video) => [
'id' => $video->id,
'title' => $video->video_title
]);
 
$courses = $tag->courses()->get()->map(fn($course) => [
'id' => $course->id,
'title' => $course->course_title
]);
 
$results = collect()->merge($courses)->merge($posts)->merge($videos);

This code will return this structure, if there is a Post/Video for the tag but no Course:

Illuminate\Support\Collection {#2198
all: [
[
"id" => 1,
"title" => "Post about Eloquent",
],
[
"id" => 1,
"title" => "Video comparing Vue and Livewire",
],
],
}

Enjoyed This Tutorial?

Get access to all premium tutorials, video and text courses, and exclusive Laravel resources. Join our community of 10,000+ developers.

Comments & Discussion

BW
Balázs Winkler ✓ Link copied!

I'm courious how to solve this issue with the modification: use only single query. It sounds more cheaper.

PK
Povilas Korop ✓ Link copied!

Since it covers three different DB tables with different structure, that one SQL query would probably be a UNION query, so not sure if it's cheaper.

AF
Adrien Foulon ✓ Link copied!

If you create a Pivot model you can actually do this easily

class Taggable extends Pivot {
public function taggable() {
return $this->morphTo();
}
}

On tag class

public function taggables() {
$this->hasMany(Taggable::class)->with('taggable');
}
$tag->taggables->pluck('taggable'); // You now have a collection of all models no matter their type
N
Nerijus ✓ Link copied!

With your approach you get the same amount of queries

CR
CJ Ronxel Cabug-os ✓ Link copied!

Found a solution to this by creating a separate Taggable model.

In your Taggable model you can add this.

public function taggable(): MorphTo
{
return $this->morphTo();
}

and in your Tag model you can add this.

public function related(): MorphOne
{
return $this->morphOne(Taggable::class, 'taggable');
}

and now your query would look like this.

$tags = Taggable::with(['taggable'])->get();
 
$tags->map(function ($tag) {
return [
'id' => $tag->taggable->id,
'title' => $tag->taggable->title
];
});
MS
Manu Sir ✓ Link copied!

This is the way.

AF
Adrien Foulon ✓ Link copied!

Except it's not, this solution is only for a single model with morphOne not at all the desired behavior of a Many to Many relationship, see above for the solution

RA
Richard A. Hoyle ✓ Link copied!

Trying to do something similar to this only in the Laravel-shift/blueprint; The Models I am trying to setup are Photo or Photoable to Client with a photo or avatar, Employee with a photo or avatar and Projects having moor then one photo or Image. Do you have any suggestions ?

 
 
models:
 
photo:
image: string
 
Photoables:
photo_id: foreignId(photos')->constrained()
photoable_id: integer (ID of client or Employee or Project)
photoable_type: string (Model name, like "App\Models\Client")
filename: string
relationships:
morphTo: Client, Employee, Project
 
Client:
photoables_id: morphToMany nullable
relationships:
morphMany: Photo
 
Employee:
photoables_id: morphToMany nullable
relationships:
morphMany: Photo
 
Project:
photoables_id: morphToMany nullable
relationships:
morphMany: Photo

Thanks for your time in advance!

M
Modestas ✓ Link copied!

I am not sure about the blueprint syntax at all (I never worked with it), but this seems to be about right.

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.