byrcsc/laravel-comments · 1.x
Troubleshooting.
The exceptions the package throws, what each one means, and the symptoms that are not exceptions at all.
Every exception the package throws extends CommentsException, so one catch
covers all of them:
use ByRcsc\LaravelComments\Exceptions\CommentsException;
try {
$post->comment($request->input('body'), by: $request->user());
} catch (CommentsException $e) {
return back()->withErrors(['body' => $e->getMessage()]);
}Configuration
InvalidConfigurationException on boot. Something in
config/comments.php is missing or malformed: a blank or missing table name, an
unknown actor_key_type, a negative max_depth, a non-integer max_length, an
unknown status, a malformed allowed_reactions, a non-string attachment disk, or
an empty notification channel list.
The message names the key and what it received. Fix the config; these are checked at boot precisely so they do not surface as a confusing query error long afterwards.
"allowed_reactions is missing." Absent is not the same as null. A
published config predating the key would otherwise turn the allowlist off
silently, so the package asks you to add it — set it to null if you meant
"accept anything".
Writing
CommentableNotPersistedException. You called comment() on a model that
has not been saved. Save it first.
ThreadTooDeepException. The reply would sit below comments.max_depth.
Nothing was written. Raise the limit, set it to null, or reply higher up the
thread.
BodyTooLongException. The body exceeds comments.max_length, counted in
characters. Nothing was written and nothing was truncated.
LogicException: Cannot reply to an unsaved comment. Persist the parent
first.
Editing
CommentTrashedException on edit. A soft-deleted comment cannot be
rewritten. Restore it first if the edit is legitimate.
LogicException: Cannot edit the body of a comment loaded without it. The
comment was selected without its body column, so there is nothing truthful to
file as a revision. Select the column, or reload the comment.
RevisionIsAppendOnlyException. Revisions refuse updates. Nothing in the
package writes to an existing revision, and neither should your application.
Reactions
InvalidReactionException. The reaction is blank, longer than 64
characters, or outside comments.allowed_reactions. Nothing was stored.
If a reaction that looks right is refused, check the exact string: emoji that can be written more than one way — a base character plus a variation selector, say — are different reactions to the allowlist and to the unique index alike.
CommentTrashedException on react or unreact. A tombstone keeps the
reactions it had and takes no changes, in either direction.
LogicException: Cannot change reactions on an unsaved comment. Persist it
first.
Attachments
InvalidAttachmentException. A blank path, a blank name, a negative size,
or an attachment that belongs to a different comment.
ImageSupportMissingException. attachImage() needs the framework's Image
facade — Laravel 13 and up — and intervention/image:
composer require intervention/imageNothing else in the package needs it; attach() works without it.
AttachmentStorageFailedException. The framework's image pipeline could not
write to the disk. Check the disk name, the directory, and its permissions.
CommentTrashedException on attach or detach. A tombstone takes no
attachment changes.
Counts
CommentsCountNotEnabledException. recountComments() was called on a model
that keeps no count. Return a column name from commentsCountColumn() to opt it
in, and add the column. See comment counts.
The count is wrong and no exception was thrown. Something wrote comments around Eloquent's model events — the query builder, an upsert, raw SQL, a restored dump. Run the repair:
php artisan comments:recount --dry-run
php artisan comments:recountcomments:recount reports nothing for a model you expected. A full sweep
visits the types the comments table mentions plus your morph map. A model whose
comments were all hard deleted, in an application with no morph map, is
invisible to it — name it with --model.
Testing
NotFakeableException. Comments::fake() records comments, replies, and
reactions, and refuses everything else. Moderating, editing, pinning, attaching,
and deleting need a real database. See testing.
A relation or scope returns nothing under the fake. Reads are not faked. Ask
the recorder — commentsOn(), repliesTo(), reactionsOn() — rather than the
database.
Symptoms that are not exceptions
Pending or rejected comments appear on a public page. The query is missing
approved(). Status is not visibility; the scope is. See
rendering and safety.
A transition returned false and nothing happened. The comment was already
in that status. Re-entering a status writes nothing and fires nothing, by
design.
No revision was recorded for an edit. The write skipped Eloquent's model
events — saveQuietly(), the query builder, raw SQL. So did the count
maintenance and the events. See edits and revisions.
No reply notification arrived. Check, in order: is
comments.notifications.reply.enabled true; is the reply approved; is the parent
comment authored by a model rather than a guest; is that model Notifiable; and
is the replier somebody other than the parent's author. reply_notified_at on
the reply tells you whether a send was already attempted — it is written once
and never cleared. See reply notifications.
A moderation listener never fires. You listened to CommentModerated.
Laravel's dispatcher resolves listeners by interface but never by parent class:
name the concrete events and type-hint the base. See
events and listeners.
Replies vanished after a delete. forceDelete() takes the whole reply
subtree through the database's cascade. delete() does not — it leaves replies
readable under a tombstone. See deleting comments.
A listener never heard about a force-deleted reply. The cascade fires no
model events. AttachmentRemoved is swept across the whole subtree by the
package, and CommentForceDeleted carries $countableRemoved for counts;
anything else you keep per comment has to walk the subtree before deleting.
Pinned comments sort differently on another database. They should not —
pinnedFirst() spells the null test out rather than leaving it to the driver.
If ordering differs, check that the query is using the scope rather than a
hand-written orderBy('pinned_at').