Polymorphic relationships
Comments can be left on posts, on videos and on products. Keeping three nearly identical tables is awkward. A polymorphic relationship lets one model belong to models of different types.
The table
Instead of post_id the table has two columns: the owner's identifier and type. The morphs() method creates them:
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->morphs('commentable');
$table->timestamps();
});The commentable_id and commentable_type columns appear.
The models
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
protected $fillable = ['body'];
public function commentable(): MorphTo
{
return $this->morphTo();
}
}use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}Using it
$post->comments()->create(['body' => 'Great post!']);
$video->comments()->create(['body' => 'Thanks for the video']);
foreach ($post->comments as $comment) {
echo $comment->body;
}
$owner = $comment->commentable; // a Post or a VideoLaravel fills in the type and the identifier by itself and returns a model of the right class.
A single record
For a one-to-one relationship — say, one image for a user and for a product — there is morphOne():
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}Many to many
Tags fit both posts and videos. The links live in one pivot table, taggables, with the tag_id, taggable_id and taggable_type columns:
class Post extends Model
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
class Tag extends Model
{
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
}$post->tags()->attach($tagId);
$post->tags()->sync([1, 2, 3]);Loading without extra queries
$comments = Comment::with('commentable')->latest()->get();
$posts = Post::withCount('comments')->get();The morph map
By default the type column stores the full class name — App\Models\Post. If the class is renamed, old records lose their owner. A morph map replaces the names with short labels:
use Illuminate\Database\Eloquent\Relations\Relation;
public function boot(): void
{
Relation::enforceMorphMap([
'post' => Post::class,
'video' => Video::class,
]);
}Common mistake
A polymorphic relationship cannot have a foreign key: the commentable_id column refers to different tables. The database will not delete comments together with a post, nor stop a reference to a non-existent record. Delete dependent records yourself — in the model's deleting event — and set the morph map from the very start of the project.