›
byrcsc/laravel-comments · 1.x
Laravel Comments adds threaded, moderated discussion to Eloquent models.
Posts may need public discussion, tickets may need internal notes, and orders may need staff remarks. Building each feature separately repeats storage, threading, moderation, and history logic.
Laravel Comments adds those capabilities to any Eloquent model through one trait. Posts, orders, tickets, and invoices can share the same polymorphic comment tables.
The package stores comments and their state. Your application keeps ownership of its UI, rendering, users, and moderation policy.
| Requirement | Supported versions |
|---|---|
| PHP | 8.3, 8.4 |
| Laravel | 12.x, 13.x |
The package follows semantic versioning: upgrading within 1.x is safe. Source
and issues live at
github.com/byrcsc/laravel-comments.
A comment belongs to one already-persisted commentable record and is written by a commentator, any Eloquent model, or by a guest identified only by a name and an email address. Exactly one of the two, never both.
Comments form threads through parent_id. Each one carries a moderation
status, and accumulates reactions, revisions, and attachments.
Two boundaries shape everything else, and both are worth knowing before you write a line:
Status is package state, not visibility. A comment is pending,
approved, rejected, or spam. The package records transitions and fires
events; what a visitor sees is decided by your queries, and the approved()
scope is the tool for making that decision.
The body is stored verbatim. No sanitization, no markdown, no rendering. Escape or render on output. Treat every body, every guest name, and every guest email as untrusted input, see rendering and safety.
The package also owns no routes, no controllers, and no views beyond one mail template. It never authorizes its own methods, and it never resolves an authenticated user on your behalf.
HasComments trait that makes any model commentable in one line, with
comment() and commentAsGuest() write methods.reply() and replyAsGuest(), with a configurable depth
limit and a depth() walk.approve(), reject(), and markAsSpam(), each idempotent and
each firing exactly one event per real state change, plus scopes for every
status.DecidesCommentStatus hook on
the commentable that beats it. Guests start pending whatever the default
says.edited_at and files an
append-only revision holding what the comment said before.attachImage() convenience that runs the framework's image pipeline.pinnedFirst() ordering scope
that behaves the same on MySQL, PostgreSQL, and SQLite.comments:recount as the repair tool.CommentPolicy,
factories for all four models, and a Comments::fake() recorder for your own
test suite.Add the trait to the model that receives comments, and write one:
use ByRcsc\LaravelComments\Concerns\HasComments;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasComments;
}
$comment = $post->comment('Great write-up!', by: $user);
$comment->reply('Agreed, especially the last section.', by: $teammate);Adding the trait is the whole integration. There is no registration step, no service to bind, and no configuration you have to touch first.
Four decisions explain most of the API.
One identity per comment. A comment names a commentator model or a guest, never both and never neither. A guest is a name and an email, stored as given and verified by nothing, which is why guests cannot react and are never notified.
Idempotent transitions. Approving an approved comment writes nothing and fires nothing. Pinning a pinned comment does the same. One event means one real state change, which is what counts and notifications downstream are built on.
Model events are the seam. Edit history, the depth limit, the length limit,
and count maintenance all ride Eloquent's model events. Anything that goes
around them, saveQuietly(), the query builder, raw SQL, goes around these
too.
The application owns the files. An attachment is a row of metadata about a file your application stored. The package never opens it, never checks it is there, and never deletes it.