byrcsc/laravel-comments · 1.x
Moderation.
The four statuses, the three transition methods, the events they fire, and the re-moderation pattern for edited comments.
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.
Move a comment
$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.
Status is not visibility
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.
Read a queue
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.
Replies carry their own status
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.
Events
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.
Re-moderate an edited comment
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().
Validate a status from a form
use ByRcsc\LaravelComments\Enums\CommentStatus;
'status' => ['required', Rule::in(CommentStatus::values())],CommentStatus::values() returns ['pending', 'approved', 'rejected', 'spam'].
Authorization
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.