›
byrcsc/laravel-comments · 1.x
Approve, reject, or mark comments as spam and query each status.
Every comment sits in exactly one of four statuses. The package records which one and fires an event when it changes; what a visitor sees stays in your queries.
| Status | Meaning |
|---|---|
pending | Waiting on a moderator |
approved | In the set a visitor generally reads |
rejected | Turned down, still stored |
spam | Kept apart from rejected, so it stays feedable to spam tooling |
There is no ordering and no workflow here. A comment may move from any status to any other. Staged sign-off is a different problem, see Laravel Approval.
$comment->approve(by: $moderator);
$comment->reject(by: $moderator);
$comment->markAsSpam(by: $moderator);The actor is optional. When you pass one it is recorded on the event, not on
the comment, the package stores no moderated_by column, because a console
command, a queued job, and a spam service are all valid moderators and none of
them is a user row.
Each method returns whether the status actually moved:
$comment->approve(); // true , it was pending
$comment->approve(); // false, nothing to doRe-entering the current status writes nothing and fires nothing. That is what makes a listener counting approvals count real ones, and what stops a double click from sending two notifications.
Nothing returns a comment to pending. The transition methods only move it out
of the queue; putting it back is a decision your application makes, see
re-moderation below.
Approving a comment does not publish it and rejecting one does not hide it. The package moves a column and fires an event. What a visitor reads is the query you wrote:
$post->comments()->approved()->topLevel()->get();Keeping that decision in the application is what lets a moderator page read
pending(), an audit view read everything, and a public page read
approved(), all from the same table.
The package ships no queue model because a scope is one:
use ByRcsc\LaravelComments\Models\Comment;
Comment::query()->pending()->with('commentable')->latest()->paginate();The scopes are pending(), approved(), rejected(), and spam(). They work
on the model globally and through a commentable's relation alike, and they
compose with the thread scopes:
$post->comments()->pending()->topLevel()->get();Soft-deleted comments are left out by Eloquent's own global scope, whatever status they carry.
Approving a comment leaves its replies exactly as they were. Each comment in a thread is moderated independently, which is what lets one off-topic reply be rejected without taking the discussion with it.
Each transition fires its own event, all three extending CommentModerated:
| Method | Event |
|---|---|
approve() | CommentApproved |
reject() | CommentRejected |
markAsSpam() | CommentMarkedAsSpam |
Every one carries the comment, the actor (or null), and $previousStatus, the
status it moved from, which a listener cannot recover from the comment itself
once the write has landed.
To treat all three alike, listen to them by name and type-hint the base:
use ByRcsc\LaravelComments\Events\CommentApproved;
use ByRcsc\LaravelComments\Events\CommentMarkedAsSpam;
use ByRcsc\LaravelComments\Events\CommentModerated;
use ByRcsc\LaravelComments\Events\CommentRejected;
use Illuminate\Support\Facades\Event;
Event::listen(
[CommentApproved::class, CommentRejected::class, CommentMarkedAsSpam::class],
fn (CommentModerated $event) => $this->reindex($event->comment),
);Laravel's dispatcher resolves listeners by interface but never by parent class,
so listening to CommentModerated alone would never fire. See
events and listeners.
An approved comment that gets edited into an advert and a typo fix look identical from inside the package, so nothing sends a comment back to the queue on its own. When your application wants that, it is a listener:
use ByRcsc\LaravelComments\Enums\CommentStatus;
use ByRcsc\LaravelComments\Events\CommentUpdated;
use Illuminate\Support\Facades\Event;
Event::listen(CommentUpdated::class, function (CommentUpdated $event): void {
if ($event->comment->wasChanged('body')) {
$event->comment->status = CommentStatus::Pending;
$event->comment->save();
}
});CommentUpdated fires after the revision for that edit is already filed, so
the listener can compare the new body against what the comment used to say. The
denormalized count follows a status moved this way, exactly as it follows
approve().
use ByRcsc\LaravelComments\Enums\CommentStatus;
'status' => ['required', Rule::in(CommentStatus::values())],CommentStatus::values() returns ['pending', 'approved', 'rejected', 'spam'].
None of these methods authorizes anything. approve() works in a seeder and a
queued job because there is nothing there to authorize against. Gate the call
site instead:
$this->authorize('approve', $comment);
$comment->approve(by: $request->user());See authorization for the shipped policy, which denies every moderation ability until you say who moderates.