Browse documentationOpen

byrcsc/laravel-comments · 1.x

Comment counts.

Opt a commentable into a denormalized count, understand what is counted and how it is maintained, and repair drift.

A listing page that shows "12 comments" next to every post should not run a count query per row. The package can keep that number on the commentable's own table, maintained in atomic database increments as comments arrive, are moderated, and are removed.

It is off by default, and stays off until you opt a model in.

Opt in

Two steps, both yours. Add the column:

Schema::table('posts', function (Blueprint $table): void {
    $table->unsignedInteger('comments_count')->default(0);
});

And name it on the model:

use ByRcsc\LaravelComments\Concerns\HasComments;

class Post extends Model
{
    use HasComments;

    public function commentsCountColumn(): ?string
    {
        return 'comments_count';
    }
}

The package writes neither, because the table is yours. There is no schema sniffing either: a package that guessed at a column would be wrong exactly when it mattered.

A model that never opted in costs nothing. Every maintenance step asks for the column, gets null, and stops there.

What is counted

Approved and not soft deleted — the number a visitor would arrive at. Pending, rejected, spam, and trashed comments are all outside the set.

Replies count. A thread of one comment and three approved replies counts as four, because that is what "12 comments" means on a listing page.

How it is maintained

Every step in or out of the countable set moves the column by that many, atomically and in the database. Nothing on the maintenance path reads a value and writes it back, so two comments approved in the same request both land.

What happenedEffect
Comment created straight to approved+1
Comment created pendingNothing, until it is approved
Status moves into the approved set+1
Status moves out of it-1
Soft delete of an approved comment-1
Restore into the approved set+1
Force delete- the whole subtree's countable total

Status changes hang off the update event rather than off the transition events, so approve() and a plain attribute save are counted the same way, and counted once. The re-moderation listener that sends an edited comment back to pending moves the count with it.

The column never goes below zero: the decrement is clamped by its own where clause rather than in PHP, so a count that has already drifted low does not go negative while a repair is still pending.

Writes go through the query builder, not Eloquent. Nothing bumps the commentable's updated_at because somebody commented — that would be the package making a decision about a table it does not own.

What drifts it

Count maintenance rides Eloquent's model events. Anything that goes around them goes around this:

Comment::query()->where('status', 'pending')->update(['status' => 'approved']);
DB::table('comments')->insert([...]);

A restored database dump, a bulk upsert, or raw SQL will leave the column behind. That is what the repair path is for.

Repair one record

$post->recountComments();  // recomputes from the comments table, returns the value now stored

It writes through the query builder, so no timestamp moves and no model event fires. The in-memory attribute is brought along, so the model you are holding does not disagree with its row.

Calling it on a model that keeps no count throws a CommentsCountNotEnabledException.

Repair in bulk

php artisan comments:recount
php artisan comments:recount --dry-run
php artisan comments:recount --model=App\\Models\\Post

It recomputes from one grouped query per model type and writes only the rows that disagree, so running it against a correct table is cheap and says so. See console commands for every option and what a sweep with no filter visits.

Scheduling it nightly is reasonable insurance for an application that also writes comments outside Eloquent:

use Illuminate\Support\Facades\Schedule;

Schedule::command('comments:recount')->dailyAt('03:00');

Keeping your own aggregate instead

If the number you need is not "approved comments on this record" — comments per author, per day, per tenant — build it from the events rather than from this column. CommentForceDeleted carries $countableRemoved precisely so an application keeping its own totals can subtract the right number without recounting. See events and listeners.