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!
Transform from beginner to Laravel expert with our personalized Coaching Class starting June 21, 2025. Limited enrollment ensures focused attention.
1-hour personalized coaching
Build portfolio applications
Industry-standard techniques
Interview prep & job guidance
Complete your application to secure your spot
Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google