Features
- Nested comments — configurable max depth; reply with
parent_id. - Reactions — like / dislike with toggle, bulk toggle, per-comment stats, “popular” queries.
- Guests — optional guest name/email; fingerprint rule for cookies.
- Moderation — status workflow;
approved()scope on queries. - Tree output —
Comments::toTree()for hierarchical JSON/API responses. - Polymorphic — any Eloquent model can use
HasComments.
Requirements
- PHP ≥ 8.1 (PHP 8.2+ required for Laravel 11 and 12).
- Laravel 10.x, 11.x, or 12.x.
- Composer.
The package is Laravel-only (Eloquent, service container, events, migrations, Artisan). It is not usable outside the Laravel ecosystem.
Compatibility matrix
| PHP | Laravel 10 | Laravel 11 | Laravel 12 |
|---|---|---|---|
| 8.1 | ✓ | — | — |
| 8.2+ | ✓ | ✓ | ✓ |
Installation
composer require fiachehr/laravel-comments-pro
Migrations
php artisan vendor:publish --provider="Fiachehr\Comments\CommentsServiceProvider" --tag=comments-migrations
php artisan migrate
Configuration
php artisan vendor:publish --provider="Fiachehr\Comments\CommentsServiceProvider" --tag=comments-config
Optional publish tags
The provider may also register:
comments-requests— Form request stubs intoapp/Http/Requests.comments-controllers/comments-routes— only if stub directories ship with your package version.
Quick start
1. Commentable models
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Fiachehr\Comments\Traits\HasComments;
class Post extends Model
{
use HasComments;
}
class Article extends Model
{
use HasComments;
}
2. Create comments
use Fiachehr\Comments\Facades\Comments;
$post = Post::find(1);
// Authenticated user (set user_id in your service layer if needed)
$comment = Comments::create([
'body' => 'This is a great post!',
], $post);
// Guest
Comments::create([
'body' => 'Nice article!',
'guest_name' => 'John Doe',
'guest_email' => 'john@example.com',
], $post);
// Nested reply
$reply = Comments::create([
'body' => 'Thanks!',
'parent_id' => $comment->id,
], $post);
3. Approve
use Fiachehr\Comments\Facades\Comments;
Comments::approve($comment);
4. Reactions
use Fiachehr\Comments\Facades\Reactions;
use Fiachehr\Comments\Enums\ReactionType;
Reactions::toggle($comment, ReactionType::LIKE);
Reactions::toggle($comment, ReactionType::DISLIKE);
5. Approved list → tree
$comments = $post->comments()->approved()->get();
$tree = Comments::toTree($comments);
6. Bulk reactions & stats
$results = Reactions::bulkToggle([1, 2, 3], ReactionType::LIKE);
$stats = Reactions::getStats($comment);
// e.g. ['likes' => 5, 'dislikes' => 2, 'total' => 7]
Configuration
File: config/comments.php (after publishing).
return [
'route_prefix' => 'api/comments',
'middleware' => ['api', 'throttle:60,1'],
'max_depth' => 5,
'auto_approve_authenticated' => true,
'reply_only_to_approved_parent' => true,
'guests' => [
'allowed' => true,
'require_email' => true,
'cookie_name' => 'guest_fingerprint',
],
];
route_prefix/middleware— reserved for wiring API routes in your app.max_depth— nesting limit enforced in the service layer.auto_approve_authenticated— skip moderation for logged-in users when enabled.reply_only_to_approved_parent— block replies under pending parents.guests.*— guest policy and cookie name for fingerprint validation.
There is no per-model override on the trait; use env-specific config files or config() overrides.
Facades & services
Comments facade → CommentsService
| Method | Description |
|---|---|
create(array $data, Model $commentable) | Create comment; supports parent_id, guest fields. |
approve(Comment $comment) | Mark approved. |
toTree(Collection $comments) | Nested array structure for APIs. |
Reactions facade → ReactionService
| Method | Description |
|---|---|
toggle(Comment, ReactionType, ?guestFp, ?userId) | Toggle like/dislike. |
remove(Reaction) | Remove a reaction row. |
getStats(Comment) | Aggregated counts. |
bulkToggle(array $ids, ReactionType, …) | Many comments at once. |
getPopular($limit, $period) | Facade alias for trending comments. |
Direct service resolution
use Fiachehr\Comments\Services\CommentsService;
use Fiachehr\Comments\Services\ReactionService;
$commentsService = app(CommentsService::class);
$reactionService = app(ReactionService::class);
$comment = $commentsService->createComment($data, $post);
$reaction = $reactionService->toggleReaction($comment, ReactionType::LIKE);
$popular = $reactionService->getPopularComments(10, '7 days');
Eloquent scopes (on Comment via relation)
$post->comments()->approved()->get();
$post->comments()->withReactions()->get(); // loads reactions + like/dislike counts
Events
Shipped event classes (under Fiachehr\Comments\Events):
CommentCreated— fired when a new comment is created; exposes theCommentmodel.ReactionToggled— carries the affectedReactionmodel ($event->reaction).
use Illuminate\Support\Facades\Event;
use Fiachehr\Comments\Events\CommentCreated;
use Fiachehr\Comments\Events\ReactionToggled;
Event::listen(CommentCreated::class, function (CommentCreated $event) {
// $event->comment
});
Event::listen(ReactionToggled::class, function (ReactionToggled $event) {
// $event->reaction
});
Register listeners in App\Providers\EventServiceProvider or bootstrap/app.php (Laravel 11+) as you prefer.
HTTP layer example
Controller
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCommentRequest;
use Fiachehr\Comments\Models\Comment;
use Fiachehr\Comments\Services\CommentsService;
use Fiachehr\Comments\Services\ReactionService;
use Fiachehr\Comments\Enums\ReactionType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Controller;
class CommentController extends Controller
{
public function __construct(
private CommentsService $commentsService,
private ReactionService $reactionService
) {}
public function store(StoreCommentRequest $request, Model $commentable)
{
$comment = $this->commentsService
->createComment($request->validated(), $commentable);
return response()->json([
'success' => true,
'comment' => $comment,
]);
}
public function approve(Comment $comment)
{
$approved = $this->commentsService->approveComment($comment);
return response()->json([
'success' => true,
'comment' => $approved,
]);
}
public function react(Comment $comment, string $type)
{
$reactionType = ReactionType::from($type);
$reaction = $this->reactionService
->toggleReaction($comment, $reactionType);
return response()->json([
'success' => true,
'reaction' => $reaction,
]);
}
public function tree(Model $commentable)
{
$comments = $commentable->comments()->approved()->get();
$tree = $this->commentsService->toTree($comments);
return response()->json($tree);
}
}
Form request
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Fiachehr\Comments\Rules\GuestFingerprint;
class StoreCommentRequest extends FormRequest
{
public function rules(): array
{
return [
'body' => 'required|string|max:1000',
'parent_id' => 'nullable|exists:comments,id',
'guest_name' => 'required_if:user_id,null|string|max:255',
'guest_email' => 'required_if:user_id,null|email|max:255',
'guest_fingerprint' => ['nullable', new GuestFingerprint()],
];
}
}
Performance & caching
Eager loading
$comments = $post->comments()
->with(['user', 'reactions', 'children'])
->approved()
->get();
Cache tree or popular lists
use Illuminate\Support\Facades\Cache;
$tree = Cache::remember("comments_tree_{$post->id}", 1800, function () use ($post) {
return Comments::toTree(
$post->comments()->approved()->get()
);
});
Security
- Use Laravel’s CSRF + session or token auth on web/API routes you expose.
- Throttle comment/reaction endpoints (see default
middlewarein config). - Validate all input via Form Requests; use
GuestFingerprintwhen allowing guests. - Eloquent parameter binding protects against SQL injection for normal usage.
Frontend (Vue example)
Assume JSON tree from tree() and POST endpoints for store/react.
<template>
<div class="comments">
<div v-for="c in comments" :key="c.id">
<p>{{ c.body }}</p>
<button type="button" @click="react(c, 'like')">
👍 {{ c.likes ?? 0 }}
</button>
<button type="button" @click="react(c, 'dislike')">
👎 {{ c.dislikes ?? 0 }}
</button>
<CommentList v-if="c.children?.length" :comments="c.children" />
</div>
</div>
</template>
<script setup>
async function react(comment, type) {
await $fetch(`/api/comments/${comment.id}/react`, {
method: 'POST',
body: { type },
});
}
</script>
route_prefix and middleware with your API client (cookies, Sanctum, or Bearer tokens).
Testing
# From package clone
./vendor/bin/phpunit tests/Unit/
In a host Laravel app, add a testsuite in phpunit.xml pointing at the vendor tests if you want php artisan test to run them:
<testsuite name="Comments">
<directory suffix="Test.php">./vendor/fiachehr/laravel-comments-pro/tests/Unit</directory>
</testsuite>
php artisan test --testsuite=Comments
Troubleshooting
- Migration errors —
php artisan config:clearthen re-run migrations; avoid duplicate publishes. - Guest validation — ensure
guest_email/guest_namerules matchconfig('comments.guests'). - Reactions not updating UI — prefer
withReactions()or refresh counts from API after toggle. - Route 404 — this package does not auto-register HTTP routes; define routes that call your controllers.
Links
- GitHub — laravel-comments-pro
- Issues
- Packagist:
fiachehr/laravel-comments-pro