ritechoice23 / laravel-reactions
A simple, flexible Laravel package for adding polymorphic reactions to any model. React with any text: like, love, care, or anything you want.
Fund package maintenance!
ritechoice23
Installs: 26
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/ritechoice23/laravel-reactions
Requires
- php: ^8.2
- illuminate/contracts: ^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2025-11-08 12:49:14 UTC
README
A simple, flexible Laravel package for adding polymorphic reactions to any Eloquent model. React with any text - like, love, care, celebrate, or even custom emojis 🔥💯. Unlimited flexibility with full polymorphic relationship support.
Table of Contents
- Features
- Installation
- Configuration
- Usage
- Practical Examples
- Important Notes
- API Reference
- Testing
- Changelog
- Contributing
- Security Vulnerabilities
- Credits
- License
Features
- Fully Polymorphic: Any model can react to any other model (User → Post, User → Comment, Team → Article, etc.)
- Flexible Reaction Types: Use any text as reaction type - "like", "love", "celebrate", "🔥", "💯", or anything you want
- Simple API: Intuitive methods like
react(),unreact(),hasReactedTo(),reactionTo() - Rich Analytics: Get reaction counts, breakdowns by type, and most popular reactions
- Expressive Scopes: Chainable query scopes like
reactedTo(),reactedWith(),mostReacted() - Update or Create: Changing a reaction automatically updates the existing one (no duplicates)
- Zero Configuration: Works out of the box with sensible defaults
- Full Test Coverage: Comprehensive Pest PHP test suite included
Installation
Install the package via composer:
composer require ritechoice23/laravel-reactions
Publish and run the migrations:
php artisan vendor:publish --tag="laravel-reactions-migrations"
php artisan migrate
Optionally, publish the config file:
php artisan vendor:publish --tag="laravel-reactions-config"
Configuration
The published config file (config/reactions.php) includes:
return [ 'table_name' => 'reactions', 'default_reaction_type' => 'like', ];
Usage
Setup Models
Add traits to your models:
use Illuminate\Database\Eloquent\Model; use Ritechoice23\Reactions\Traits\CanReact; use Ritechoice23\Reactions\Traits\HasReactions; class User extends Model { use CanReact; // Can react to other models } class Post extends Model { use HasReactions; // Can receive reactions } class Comment extends Model { use CanReact; // Can react to posts use HasReactions; // Can also receive reactions }
Basic Operations
// React to a model with default type (like) $user->react($post); // React with a specific type $user->react($post, 'love'); $user->react($comment, 'celebrate'); $user->react($article, '🔥'); // Change reaction (automatically updates) $user->react($post, 'like'); $user->react($post, 'love'); // Changes from 'like' to 'love' // Remove reaction $user->unreact($post); // Check if user has reacted if ($user->hasReactedTo($post)) { // User has reacted to the post } // Get the reaction type $reactionType = $user->reactionTo($post); // Returns 'love' or null
Working with Reactions on Models
Get Reaction Statistics
// Get total reaction count $count = $post->reactionsCount(); // Get reaction breakdown by type $breakdown = $post->reactionsBreakdown(); // Returns: ['like' => 45, 'love' => 32, 'celebrate' => 18, '🔥' => 12] // Check if a specific user has reacted if ($post->isReactedBy($user)) { // This user has reacted to the post } // Get a specific user's reaction $reaction = $post->reactionBy($user); // Returns Reaction model or null if ($reaction) { echo $reaction->reaction_type; // 'love' echo $reaction->created_at; // When they reacted } // Remove a specific user's reaction $post->removeReaction($user);
Accessing Reaction Relationships
// Get all reactions for a post $reactions = $post->reactions; // Collection of Reaction models // Get all reactions given by a user $reactionsGiven = $user->reactionsGiven; // Collection of Reaction models // Eager load relationships $posts = Post::with('reactions')->get(); // Access reactor and reactable foreach ($post->reactions as $reaction) { $reactor = $reaction->reactor; // The User who reacted $reactable = $reaction->reactable; // The Post that was reacted to $type = $reaction->reaction_type; // The reaction type }
Query Scopes
Find models based on reaction relationships:
// Find all users who reacted to a post $users = User::reactedTo($post)->get(); // Find all users who reacted with a specific type $loveUsers = User::reactedWith($post, 'love')->get(); // Chain with other queries $activeUsers = User::reactedTo($post) ->where('status', 'active') ->orderBy('created_at', 'desc') ->get();
Advanced Queries
Eager Loading Reaction Counts
// Load posts with reaction counts $posts = Post::withReactionsCount()->get(); foreach ($posts as $post) { echo $post->reactions_count; }
Get Most Reacted Posts
// Get top 10 most reacted posts $topPosts = Post::mostReacted(10)->get(); // Custom limit $topPosts = Post::mostReacted(5)->get(); // Combine with other queries $topRecentPosts = Post::mostReacted(10) ->where('created_at', '>', now()->subWeek()) ->get();
Check Reaction Status for Current User
// Add reaction status for a specific user to query results $posts = Post::withReactionStatus($currentUser)->get(); foreach ($posts as $post) { // Check if current user has reacted if ($post->has_reacted) { echo "You reacted with: " . $post->reactor_reaction_type; } else { echo "You haven't reacted yet"; } }
Filtering Reactions
The Reaction model includes useful scopes:
use Ritechoice23\Reactions\Models\Reaction; // Get all reactions by type $loveReactions = Reaction::byType('love')->get(); // Get all reactions by a specific reactor $userReactions = Reaction::byReactor($user)->get(); // Get all reactions to a specific model $postReactions = Reaction::byReactable($post)->get(); // Combine filters $userLoveReactions = Reaction::byReactor($user) ->byType('love') ->get();
Polymorphic Reactions
React to any model type:
// Users reacting to posts $user->react($post, 'love'); // Users reacting to comments $user->react($comment, 'like'); // Teams reacting to articles (if Team uses CanReact) $team->react($article, 'celebrate'); // Comments reacting to posts (nested reactions) $comment->react($post, '🔥');
Custom Reaction Types
Use any text as a reaction type:
// Standard reactions $user->react($post, 'like'); $user->react($post, 'love'); $user->react($post, 'care'); $user->react($post, 'wow'); $user->react($post, 'sad'); $user->react($post, 'angry'); // Custom text $user->react($post, 'inspired'); $user->react($post, 'mindblown'); $user->react($post, 'thankful'); // Emojis $user->react($post, '🔥'); $user->react($post, '💯'); $user->react($post, '❤️'); $user->react($post, '😂'); $user->react($post, '🎉'); // The sky's the limit! $user->react($post, 'chef-kiss'); $user->react($post, 'big-brain');
Practical Examples
Social Media Feed
// Display post with reactions class PostController extends Controller { public function show(Post $post) { $post->load('reactions'); $breakdown = $post->reactionsBreakdown(); $totalReactions = $post->reactionsCount(); $userReaction = $post->reactionBy(auth()->user()); return view('posts.show', compact('post', 'breakdown', 'totalReactions', 'userReaction')); } public function react(Post $post, Request $request) { $request->validate([ 'type' => 'required|string|max:50' ]); auth()->user()->react($post, $request->type); return back()->with('success', 'Reacted!'); } public function unreact(Post $post) { auth()->user()->unreact($post); return back()->with('success', 'Reaction removed!'); } }
Trending Content Dashboard
// Get trending posts public function trending() { // Posts with most reactions in the last 24 hours $trendingPosts = Post::mostReacted(10) ->where('created_at', '>', now()->subDay()) ->with(['reactions' => function($query) { $query->select('reactable_id', 'reaction_type') ->selectRaw('count(*) as count') ->groupBy('reactable_id', 'reaction_type'); }]) ->get(); return view('trending', compact('trendingPosts')); }
User Profile Reactions
// Show all posts a user has reacted to public function userReactions(User $user) { $reactedPosts = Post::whereHas('reactions', function($query) use ($user) { $query->byReactor($user); }) ->with(['reactions' => function($query) use ($user) { $query->byReactor($user); }]) ->paginate(20); return view('profile.reactions', compact('user', 'reactedPosts')); }
Reaction Analytics
// Get analytics for a post public function analytics(Post $post) { $breakdown = $post->reactionsBreakdown(); $totalReactions = $post->reactionsCount(); // Get reactions over time $reactionsTimeline = $post->reactions() ->selectRaw('DATE(created_at) as date, reaction_type, count(*) as count') ->groupBy('date', 'reaction_type') ->orderBy('date') ->get() ->groupBy('date'); // Get top reactors $topReactors = Reaction::byReactable($post) ->with('reactor') ->get() ->groupBy('reactor_id') ->map(fn($reactions) => [ 'reactor' => $reactions->first()->reactor, 'count' => $reactions->count() ]) ->sortByDesc('count') ->take(10); return view('analytics', compact('post', 'breakdown', 'totalReactions', 'reactionsTimeline', 'topReactors')); }
Important Notes
Update or Create Behavior
When a model reacts to the same model again with a different type, the package automatically updates the existing reaction instead of creating a duplicate:
$user->react($post, 'like'); // Database: 1 reaction of type 'like' $user->react($post, 'love'); // Database: Still 1 reaction, now type 'love' (updated, not duplicated)
Unique Constraint
The migration includes a unique constraint to prevent duplicates at the database level:
$table->unique(['reactor_type', 'reactor_id', 'reactable_type', 'reactable_id'], 'unique_reaction');
This ensures one reactor can only have one reaction per reactable model at any time.
Performance Considerations
The package uses optimized database queries:
- Indexed columns: All foreign keys and reaction types are indexed
- Eager loading support: Use
with()andwithCount()to avoid N+1 queries - Efficient scopes: Query scopes use optimized joins and subqueries
- Unique constraints: Prevents duplicate data at the database level
// ✅ Good - Efficient $posts = Post::with('reactions') ->withReactionsCount() ->mostReacted(10) ->get(); // ❌ Bad - N+1 queries $posts = Post::all(); foreach ($posts as $post) { $count = $post->reactions()->count(); // N+1 query }
Database Indexes
The migration includes optimized indexes for performance:
- Unique composite index on reactor and reactable (prevents duplicates)
- Index on reactable_type and reactable_id (for lookups)
- Index on reactor_type and reactor_id (for reverse lookups)
- Index on reaction_type (for filtering by type)
Reaction Validation
Validate reaction types in your controller:
public function react(Post $post, Request $request) { $request->validate([ 'type' => 'required|in:like,love,celebrate,wow,sad,angry' ]); auth()->user()->react($post, $request->type); return response()->json(['success' => true]); }
API Reference
CanReact Trait Methods
| Method | Parameters | Return | Description |
|---|---|---|---|
react() |
Model $model, ?string $type = null |
Reaction |
React to a model (creates or updates) |
unreact() |
Model $model |
bool |
Remove reaction from a model |
hasReactedTo() |
Model $model |
bool |
Check if has reacted to a model |
reactionTo() |
Model $model |
?string |
Get reaction type to a model |
reactionsGiven() |
- | MorphMany |
Relationship: all reactions given |
CanReact Trait Scopes
| Scope | Parameters | Description |
|---|---|---|
reactedTo() |
Model $model |
Models that reacted to a specific model |
reactedWith() |
Model $model, string $type |
Models that reacted with a specific type |
HasReactions Trait Methods
| Method | Parameters | Return | Description |
|---|---|---|---|
reactions() |
- | MorphMany |
Relationship: all reactions received |
reactionsCount() |
- | int |
Total number of reactions |
reactionsBreakdown() |
- | array |
Reaction count by type |
isReactedBy() |
Model $reactor |
bool |
Check if reacted by a specific model |
reactionBy() |
Model $reactor |
?Reaction |
Get reaction by a specific model |
removeReaction() |
Model $reactor |
bool |
Remove a specific model's reaction |
HasReactions Trait Scopes
| Scope | Parameters | Description |
|---|---|---|
withReactionsCount() |
- | Eager load reaction count |
mostReacted() |
int $limit = 10 |
Order by most reacted |
withReactionStatus() |
Model $reactor |
Add reaction status for a specific reactor |
Reaction Model Scopes
| Scope | Parameters | Description |
|---|---|---|
byType() |
string $type |
Filter reactions by type |
byReactor() |
Model $reactor |
Filter reactions by reactor |
byReactable() |
Model $reactable |
Filter reactions by reactable |
Testing
Run the test suite:
composer test
Run tests with coverage:
composer test-coverage
Run static analysis:
composer analyse
Run code formatting:
composer format
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.