byrcsc/laravel-comments · 1.x
Deleting comments.
Soft deletes leave readable tombstones with their replies intact; force deletes take the whole subtree through the database cascade.
Comments soft delete. delete() marks a tombstone, restore() brings it back,
and forceDelete() removes it for good — along with everything below it in the
thread.
Soft delete
$comment->delete();The comment leaves every ordinary query through Eloquent's global scope. What it leaves behind is deliberate:
- Its replies stay readable. They are not soft deleted with it. A thread does not disappear because one comment in the middle of it did.
- Its reactions, revisions, and attachment rows stay. A moderator reading what happened needs them.
- Those three become read-only. Attaching, detaching, reacting, unreacting,
and editing all throw a
CommentTrashedExceptionon a tombstone. A record kept as history that can still be changed is not history.
Reading is never gated. reactionSummary(), revisions, and attachments all
work on a tombstone.
Read tombstones back the usual way:
Comment::withTrashed()->find($id);
Comment::onlyTrashed()->get();Restore
$comment->restore();The comment comes back exactly as it was, status included, and its reactions and attachments become writable again.
Force delete
$comment->forceDelete();This removes the comment and its entire reply subtree, through the
database's cascade on parent_id — soft-deleted replies included, because the
cascade does not care whether a row was a tombstone. Each removed comment takes
its own reactions, revisions, and attachment rows with it through their own
cascades.
Ancestors are untouched. Force deleting a mid-thread comment leaves the comment it replied to exactly where it was.
What the cascade does not fire
The database's cascade fires no model events. The replies it removes dispatch
nothing of their own — no CommentDeleted, no CommentForceDeleted.
The package works around that in the two places it matters, and you may have to in a third:
Attachments. Before the cascade runs, the package walks the whole subtree
and fires AttachmentRemoved once per attachment on it. A file-cleanup listener
sees every file, not just the ones on the comment you deleted.
Counts. CommentForceDeleted carries $countableRemoved: how many comments
in that subtree were approved and not soft deleted at the moment they went, this
one included. It cannot be recovered afterwards — by the time the event fires,
the rows are gone.
use ByRcsc\LaravelComments\Events\CommentForceDeleted;
use Illuminate\Support\Facades\Event;
Event::listen(CommentForceDeleted::class, function (CommentForceDeleted $event): void {
$this->stats->decrement('comments', $event->countableRemoved);
});Anything else you keep per comment. A search index, an activity feed, a
cache key — a listener that must see every removed row has to walk the subtree
itself, before calling forceDelete().
Events
| Call | Events |
|---|---|
delete() | CommentDeleted |
restore() | CommentRestored |
forceDelete() | CommentDeleted, then CommentForceDeleted |
CommentDeleted fires on both kinds of delete, mirroring Eloquent's own
deleted event. Listen to CommentForceDeleted when only permanent removal
matters.
Both fire for the comment you called the method on. Neither fires for the replies the cascade takes.
Counts
The denormalized count follows all three operations: a soft delete subtracts an
approved comment, a restore adds it back if it is still approved, and a force
delete subtracts the whole subtree in one atomic step using
$countableRemoved. See comment counts.
Authorization
delete allows the comment's own author by default. restore and forceDelete
both deny for everybody:
- Restoring is a moderator's undo, not an author's. A comment removed for a reason should not come back because the person who wrote it wants it to.
- Force deleting destroys a comment, its replies, its reactions, its revisions, and its attachment rows. Nothing about that should be available by default.
See authorization.