Browse documentationOpen

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 CommentTrashedException on 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

CallEvents
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.