byrcsc/laravel-comments ยท 1.x
Reactions.
React, unreact, and toggle; render counts in one query; configure the allowlist; and know why there is no guest reactor.
A reaction is one reactor's one reaction to one comment โ a thumbs up, a heart, a custom key your interface understands. The database enforces the uniqueness, so a double tap on a slow connection cannot inflate a count.
React
$comment->react('๐', by: $user); // returns the CommentReaction row
$comment->unreact('๐', by: $user); // returns whether there was one to remove
$comment->toggleReaction('๐', by: $user); // returns whether they now hold ittoggleReaction() is what one tap in an interface maps to: true when the
reaction was added, false when it was removed.
Reacting again with the same reaction is a no-op that hands back the existing row, so callers need no guard of their own. If two requests race, the unique index settles it and the loser still gets the no-op it was promised.
$by is required and must be a model. There is no guest path here:
deduplication needs an identity, and a name and an email are not one.
Render counts
reactionSummary() gives reaction-to-count, in a stable order, for the
reactions a comment actually has. A reaction nobody used is absent rather than
zero:
$comment->reactionSummary(); // ['๐' => 3, '๐' => 1]It reads the reactionCounts relation, which groups in the database. Eager
load it across a thread and rendering every comment's reactions costs one query
for the page rather than one per comment:
$comments = $post->comments()
->approved()
->with('reactionCounts')
->get();
foreach ($comments as $comment) {
$comment->reactionSummary(); // no further queries
}The reactions relation is the full rows, for the times you need the reactors
themselves rather than the totals.
Highlight what a reactor already pressed
$comment->hasReactionFrom($user); // reacted at all?
$comment->hasReactionFrom($user, '๐'); // reacted with this one?
$comment->reactionsBy($user); // ['๐', '๐'] โ sortedAll three answer from the reactions relation when it is loaded, and query
when it is not โ so eager-loading reactions alongside reactionCounts keeps a
per-comment "did I react?" check off the database.
The allowlist
comments.allowed_reactions decides what a comment accepts:
'allowed_reactions' => ['๐', '๐', 'โค๏ธ', '๐', '๐', '๐ข'],Reacting with anything outside the list throws an InvalidReactionException and
stores nothing. The check runs at the engine boundary rather than in a form
request, so a reaction that arrived another way โ a console command, a queued
import, a second interface โ is held to the same set.
Set it to null to accept any non-empty string that fits the column:
'allowed_reactions' => null,Two rules still apply with the allowlist off: a blank reaction is refused, and so is one longer than 64 characters. A truncated reaction would silently become a different one.
Emoji are compared as given. A base character plus a variation selector and the same emoji without one are two different reactions to the database, in the allowlist and in the unique index alike. List the exact form your interface sends.
Deleted comments
Reactions are frozen on a tombstone, both directions. A soft-deleted comment
keeps the reactions it already had โ the moderator judging what happened needs
them โ and refuses new ones and removals with a CommentTrashedException.
Restoring the comment makes it reactable again.
Force deleting removes the reaction rows through the database's cascade, for the comment and its whole reply subtree.
Reading is never gated: reactionSummary() works on a tombstone.
Events
| Method | Event |
|---|---|
react() | ReactionAdded |
unreact() | ReactionRemoved |
toggleReaction() | Whichever applied |
Both extend CommentReacted and carry the comment, the reactor model, and the
reaction string as stored. Neither fires for a no-op, so one event means one
real change to what the comment carries.
What reactions are not
Reactions belong to comments and to nothing else. There is no general "reactable" trait here, and no way to react to a post, a photo, or a user. A package that grew one would be two packages.
Authorization
react is an ability on the shipped CommentPolicy and allows any
authenticated actor by default โ including on somebody else's comment, which is
the point. See authorization.