Browse documentationOpen

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.

StatusMeaning
pendingWaiting on a moderator
approvedIn the set a visitor generally reads
rejectedTurned down, still stored
spamKept 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 do

Re-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:

MethodEvent
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.