Courses

Structuring Databases in Laravel 11

Polymorphic Many-to-Many: "Trick" To Query Data

Summary of this lesson:
- Understanding morphToMany relationships
- Managing multiple model types
- Implementing morphedByMany
- Handling combined query results

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",
],
],
}
Previous: HasMany Delete Parents: Validate or Cascade
avatar

Are databases with polymorphic relations scalable?

avatar

Could you explain what you have in mind by that?

avatar

I mean what disadvandages of polymorphic relations can expect me apart from inability to control data through SQL, possible data inconsistence and orphan entries? What if I decide to shard my database in the future, will it be hard to deal with this kind of relationships?

avatar

So that's a bit more complicated than we are comfortable suggesting you.

But generally, you will have orphans in your database if you don't manage/handle it correctly yourself. As for sharding - you might encounter some issues, but not a lot. As long as database/laravel knows where to point for specific records you might be okay.

avatar
You can use Markdown
avatar
You can use Markdown