NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Comments for Laravel

Writing comments

The fluent builder

Comments::on() starts a fluent builder that reads like a sentence; post() persists and returns the Comment:

use RoundlyConsulting\Comments\Facades\Comments;

$comment = Comments::on($post)
    ->as($user)
    ->body('Nice write-up!')
    ->post();

// A reply, fluently (the parent must belong to the same subject):
Comments::on($post)->as($user)->reply($comment)->body('Thanks!')->post();

// Hidden comment:
Comments::on($post)->as($user)->visible(false)->body('Internal note')->post();

// Anonymous / guest comment — omit ->as():
Comments::on($post)->body('Great read.')->post();
  • as($author) — the author; omit it to record an anonymous/guest comment.
  • body($body) — the comment body, trimmed before it is stored.
  • visible($visible = true) — the visibility flag, separate from the moderation status.
  • reply($parent) — make the comment a reply; the parent must belong to the same subject.
  • post() — validate, persist and return the Comment.

Typed DTO

For queued jobs or service code, pass a WriteCommentData DTO to Comments::write():

use RoundlyConsulting\Comments\DataTransferObjects\WriteCommentData;
use RoundlyConsulting\Comments\Facades\Comments;

$comment = Comments::write(new WriteCommentData(
    commentable: $post,
    body: 'From a job',
    author: $user,        // optional
    parent: $rootComment, // optional reply — must belong to $post
));

From the author

GivesComments adds a writeComment() shortcut. Pass the model being commented on, the body and an optional visibility flag (defaults to true). It goes through the same manager as the facade:

$comment = $user->writeComment(
    commentable: $post,
    comment: 'This is a very good blog post, thanks for sharing!',
    visible: true, // optional, defaults to true
);   // goes through the same manager as Comments::on($post)->as($user)->…->post()

What every write checks

The builder, write() and the trait all run the same WriteCommentAction, in this order:

  • Parent — a reply whose parent belongs to another subject throws InvalidCommentParentException, before authorization runs.
  • Authorization — the create ability, only when comments.authorization is on.
  • Body — trimmed; an empty body or one longer than max_length throws InvalidCommentBodyException.
  • Reply depth — a reply deeper than max_depth throws MaxReplyDepthExceededException; soft-deleted ancestors still count.
  • Locks — a locked subject, or a reply anywhere inside a locked thread, throws CommentsLockedException.
  • Status — approved, or pending when require_approval is on; a blocklist match rejects, holds or hides the comment.
  • Mentions — @handles in the body are stored and resolved; CommentMentioned fires only once the comment is approved.

CommentCreated is dispatched when the comment is saved. Every failure is a subclass of CommentsException, so a single catch turns them into a form error:

use Illuminate\Http\Request;
use RoundlyConsulting\Comments\Exceptions\CommentsException;
use RoundlyConsulting\Comments\Facades\Comments;

public function store(Request $request, Post $post)
{
    try {
        Comments::on($post)
            ->as($request->user())
            ->body((string) $request->input('body'))
            ->post();
    } catch (CommentsException $e) {
        return back()->withErrors(['body' => $e->getMessage()])->withInput();
    }

    return back();
}

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.