Browse documentationOpen

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 it

toggleReaction() 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);                // ['๐Ÿ‘', '๐ŸŽ‰'] โ€” sorted

All 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

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