Kritim Yantra
Apr 06, 2025
Polymorphic comments allow users to comment on different types of content (Posts, Videos, Products) using a single comments
table. This is a clean, efficient approach compared to creating separate comment tables for each content type.
In this guide, we'll build a complete polymorphic comment system with:
php artisan make:migration create_comments_table
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->unsignedBigInteger('user_id'); // Comment author
$table->unsignedBigInteger('commentable_id'); // Polymorphic ID
$table->string('commentable_type'); // Polymorphic class
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
php artisan make:migration create_posts_table
php artisan make:migration create_videos_table
// Posts table
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
// Videos table
Schema::create('videos', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('url');
$table->timestamps();
});
Run migrations:
php artisan migrate
php artisan make:model Comment
class Comment extends Model
{
protected $fillable = ['body', 'user_id'];
public function commentable()
{
return $this->morphTo();
}
public function user()
{
return $this->belongsTo(User::class);
}
}
// Post.php
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
// Video.php
class Video extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
php artisan make:controller CommentController
class CommentController extends Controller
{
public function store(Request $request)
{
$request->validate([
'body' => 'required',
'commentable_id' => 'required',
'commentable_type' => 'required|in:App\Models\Post,App\Models\Video'
]);
$commentable = $request->commentable_type::find($request->commentable_id);
$comment = $commentable->comments()->create([
'body' => $request->body,
'user_id' => auth()->id()
]);
return back()->with('success', 'Comment added!');
}
public function destroy(Comment $comment)
{
$this->authorize('delete', $comment);
$comment->delete();
return back()->with('success', 'Comment deleted!');
}
}
// web.php
Route::post('/comments', [CommentController::class, 'store'])->name('comments.store');
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
<!-- resources/views/comments/_form.blade.php -->
@auth
<form action="{{ route('comments.store') }}" method="POST">
@csrf
<input type="hidden" name="commentable_id" value="{{ $model->id }}">
<input type="hidden" name="commentable_type" value="{{ get_class($model) }}">
<div class="mb-3">
<textarea name="body" class="form-control" placeholder="Add a comment..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Post Comment</button>
</form>
@endauth
<!-- resources/views/comments/_list.blade.php -->
<div class="comments mt-4">
@foreach($model->comments()->with('user')->latest()->get() as $comment)
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between">
<h6>{{ $comment->user->name }}</h6>
<small>{{ $comment->created_at->diffForHumans() }}</small>
</div>
<p>{{ $comment->body }}</p>
@can('delete', $comment)
<form action="{{ route('comments.destroy', $comment) }}" method="POST">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
@endcan
</div>
</div>
@endforeach
</div>
<!-- resources/views/posts/show.blade.php -->
@extends('layouts.app')
@section('content')
<article>
<h1>{{ $post->title }}</h1>
<p>{{ $post->content }}</p>
<!-- Include comments -->
@include('comments._list', ['model' => $post])
@include('comments._form', ['model' => $post])
</article>
@endsection
Create a CommentPolicy:
php artisan make:policy CommentPolicy --model=Comment
class CommentPolicy
{
public function delete(User $user, Comment $comment)
{
return $user->id === $comment->user_id;
}
}
Register in AuthServiceProvider:
protected $policies = [
Comment::class => CommentPolicy::class,
];
$post = Post::create(['title' => 'First Post', 'content' => 'Post content']);
$video = Video::create(['title' => 'Demo Video', 'url' => 'video1.mp4']);
// Add comments
$post->comments()->create(['body' => 'Great post!', 'user_id' => 1]);
$video->comments()->create(['body' => 'Nice video!', 'user_id' => 1]);
/posts/1
and /videos/1
to see comments working for both types.// Add to comments table
$table->unsignedBigInteger('parent_id')->nullable();
// In Comment model
public function replies()
{
return $this->hasMany(Comment::class, 'parent_id');
}
// In CommentController
$commentable->user->notify(new NewCommentNotification($comment));
Use Laravel Echo with WebSockets to show new comments in real-time.
You've now built a fully functional polymorphic comment system that can:
✅ Attach to any model (Posts, Videos, etc.)
✅ Handle user authorization
✅ Display threaded comments
✅ Scale easily with new content types
🚀 Your application now has a professional, flexible commenting system!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google