Back to Course |
Laravel 11 Eloquent: Expert Level

Polymorphic Relations: Explained with Example

Polymorphic relations are challenging to understand at first glance. So, the best way to explain it is with examples. How do we use that structure without polymorphic relations?


Non-flexible Structure, or Polymorphic?

Imagine you have a users table and then a projects table, and then those projects are divided into tasks. So, there are three database tables, and you want to add a database table called photos.

The photo may belong to a project or a task. So, the tables could be project_photos, task_photos.

But what if a user also could have a photo-like avatar. So, another table would be user_photos? Each table would have a corresponding foreign key column like project_id, task_id, and user_id.

That's one way, but you probably feel something is wrong because it's all repeating except for the foreign ID field.

project_photos
- id
- filename
- project_id
- timestamps
 
task_photos
- id
- filename
- task_id
- timestamps
 
user_photos
- id
- filename
- user_id
- timestamps

Maybe there should be one table, photos? But then, foreign key to what? Another way is to have foreign keys belonging to all of them. So, three fields, one of which will be filled out, and the other ones will be null.

photos
- id
- filename
- project_id
- task_id
- user_id
- timestamps

But the problem with that approach, in addition to too many fields in the database with meaningless null data often, is what if you have a need for another table and the fourth field? So, the photo will belong to a post in the future. What about the fifth one? So, this is not a flexible structure.

Polymorphic relations are for any database table that belongs to many other dynamic tables.

So, the solution in polymorphic relations would transform the table of photos from all those foreign IDs to only two columns. What do we need to know in these two columns? The ID and type of that record are usually models.

photos
- id
- filename
- model_id
- model_type
- timestamps

The name of these two fields should be the Model's name with a suffix of able, then underscored with the ID and type.

photos
- id
- filename
- photoable_id
- photoable_type
- timestamps

So, the photoable_id is an integer. But it's not a foreign key, and that's the important thing about polymorphic relations. They are not relations on the database level, so there is no foreign key here. It's a relation in terms of Laravel structure in Eloquent. And, photoable_type is a string defining the Eloquent Model of that object.

The Migration would look like this:

Schema::create('photos', function (Blueprint $table) {
$table->id();
$table->string('filename');
$table->unsignedBigInteger('photoable_id');
$table->string('photoable_type');
$table->timestamps();
});

Laravel has a shorter way of creating polymorphic columns in the Migration using the morphs() method and passing the column name.

Schema::create('photos', function (Blueprint $table) {
$table->id();
$table->string('filename');
$table->morphs('photoable');
$table->timestamps();
});

Then, the main thing is an Eloquent Model. In this example, the Photo Model must have the morphTo() relation.

app/Models/Photo.php:

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Photo extends Model
{
public function photoable(): MorphTo
{
return $this->morphTo();
}
}

This means that any Eloquent Model can morph into those photos. Then, in other models, you must define the relationship between photos and provide the morphable name. For example, for the Task Model:

app/Models/Task.php:

use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Task extends Model
{
public function photos(): MorphMany
{
return $this->morphMany(Photo::class, 'photoable');
}
}

This example is a one-to-many relation where one task can have many photos. The exact relation for photos would be for every Model.

app/Models/User.php:

use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class User extends Authenticatable
{
// ...
 
public function photos(): MorphMany
{
return $this->morphMany(Photo::class, 'photoable');
}
}

Using Polymorphic Relations

How do you use it in Controllers or elsewhere? The usage isn't different from other relations. You eager load or call them the same way.

$users = User::with('photos')->get();
 
foreach ($users as $user) {
print('<div>' . $user->id . ': ' . $user->name . ' (');
foreach ($user->photos as $photo) {
print($photo->filename . ', ');
}
print(')</div>');
}
 
print(' <hr />');
 
$tasks = Task::with('photos')->get();
 
foreach ($tasks as $task) {
print('<div>' . $task->id . ': ' . $task->description . ' (');
foreach ($task->photos as $photo) {
print($photo->filename . ', ');
}
print(')</div>');
}

I have seeded some data to the photos table, and as you can see, the type is the full path to the Model, and the ID is from that record.

I have three users and nine tasks in the database, each with two photos. I can see the result when I execute the code in the browser.


Quick Tip: Enforce Models

If you don't want the full path to the Model as a type, you can change it by calling the enforceMorphMap method in the boot method of the AppServiceProvider.

app/Providers/AppServiceProvider.php:

use Illuminate\Database\Eloquent\Relations\Relation;
 
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
Relation::enforceMorphMap([
'user' => 'App\Models\User',
'task' => 'App\Models\Task',
]);
}
}

Now, in the database, the type is set respectively.