Browse documentationOpen

byrcsc/laravel-comments · 1.x

Edits and revisions.

Every body change stamps edited_at and files an append-only revision — including plain saves, and excluding anything that skips model events.

Editing a comment's body records what it used to say. That happens through Eloquent's model events, so it covers edit(), update(), and a plain attribute save alike.

Edit a comment

$comment->edit('A clearer version', by: $user);

edit() exists so the editor gets named. Returns whether anything was saved — editing a comment to the body it already has writes nothing and records nothing.

A plain save records the same history with a null editor, because the package will not invent an actor:

$comment->update(['body' => 'A clearer version']);

$comment->body = 'A clearer version';
$comment->save();

A null editor is ordinary: a console command, a queued job, and an import all change a body with nobody to name.

Read the history

$comment->edited_at;   // when the body last changed, or null
$comment->revisions;   // prior bodies, oldest first

Each revision holds the body as it stood before that edit, plus the editor when one was named:

foreach ($comment->revisions as $revision) {
    $revision->body;      // what it used to say
    $revision->editor;    // the model that made the change, or null
    $revision->created_at;
}

The current body lives on the comment. A comment edited twice has two revisions: the original text and the intermediate one.

editor is an ordinary polymorphic relation, so with('revisions.editor') loads a whole history's authors in one go.

What counts as an edit

Only a body change is gated, stamps edited_at, and earns a revision. Moderating, pinning, restoring, and every other update leave all three alone: they change what the package knows about the comment, not what its author wrote.

Saving the same body over itself is not an edit either.

What is not recorded

Recording rides Eloquent's model events, so anything that skips them skips this:

$comment->body = 'Rewritten';
$comment->saveQuietly();                                    // no revision

Comment::query()->where('id', $comment->id)
    ->update(['body' => 'Rewritten']);                      // no revision

That is the same seam comment counts ride, and the same caveat applies: writes that go around Eloquent go around the package.

The gates an edit shares

An edit passes the same checks a write does, and two of its own:

  • comments.max_length is re-checked, so an edit cannot do what a write was refused. Exceeding it throws a BodyTooLongException.
  • A tombstone is refused. Editing a soft-deleted comment throws a CommentTrashedException — a comment kept as history that can still be rewritten is not history.
  • An unsaved comment is refused, with a LogicException. Persist it first.
  • A comment loaded without its body is refused, also with a LogicException. The one job of a revision is to say what the comment used to hold, and a comment selected without body cannot answer that. Reload it, or select the column.

Revisions are append-only

The CommentRevision model refuses updates outright:

use ByRcsc\LaravelComments\Exceptions\RevisionIsAppendOnlyException;

$revision->update(['body' => 'Never happened']);  // throws

The refusal is a model event, so every path through Eloquent hits it. Deleting a revision is still allowed, and there is no updated_at column — a row that can never be updated has no honest second timestamp to keep.

This is a convention, not a proof. There is no hash chain here and no tamper evidence: the rows are ordinary rows, and anything with database access can edit them. An application that needs a verifiable history wants Laravel Approval.

Deletion

Soft deleting a comment keeps its revisions, which is what lets a moderator read what a removed comment used to say. Force deleting removes them through the cascade, for the comment and its whole subtree.

Re-moderating an edit

CommentUpdated fires after the revision for that edit is filed, so a listener can compare the new body against what the comment said before and send it back to the queue. See moderation.

Seed history in a test

use ByRcsc\LaravelComments\Models\CommentRevision;

CommentRevision::factory()->forComment($comment)->by($editor)->create();

That is for tests about reading history. A test about recording it should edit the comment and let the engine file the row.