›
byrcsc/laravel-comments · 1.x
Record append-only revision history when a comment body is edited.
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.
$comment->edit('A clearer version', by: $user);edit() records the editor and returns whether anything was saved. Submitting
the existing body writes no change and records no revision.
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.
$comment->edited_at; // when the body last changed, or null
$comment->revisions; // prior bodies, oldest firstEach 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.
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.
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 revisionThat is the same seam comment counts ride, and the same caveat applies: writes that go around Eloquent go around the package.
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.CommentTrashedException, a comment kept as history that can still be
rewritten is not history.LogicException. Persist it first.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.The CommentRevision model refuses updates outright:
use ByRcsc\LaravelComments\Exceptions\RevisionIsAppendOnlyException;
$revision->update(['body' => 'Never happened']); // throwsThe 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.
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.
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.
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.