cybercog/laravel-love
Add reactions, likes, votes, and other “feelings” to any Eloquent model with Laravel Love. Flexible, enterprise-ready system inspired by GitHub/Facebook/Slack reactions. Includes migrations and APIs to make models reactable in minutes.
Installation:
composer require cybercog/laravel-love
php artisan migrate
Run migrations to create love_reactions, love_reaction_totals, and love_reactant_reaction_totals tables.
Enable Reactions on a Model:
Use the make:reactable Artisan command:
php artisan make:reactable Post
This generates a migration for the love_reactant_id column and adds the Reactable trait to your model.
Define Reaction Types:
In your Post model, define allowed reactions (e.g., like, dislike):
use Cog\Laravel\Love\Traits\Reactable;
class Post extends Model
{
use Reactable;
protected $reactable = [
'like', 'dislike', 'love', 'haha'
];
}
First Reaction: In a controller or service, react to a post:
$post = Post::find(1);
$user = auth()->user();
$user->reactTo($post, 'like');
Display Reaction Counts:
$post = Post::find(1);
echo $post->reactionsCount; // Total reactions
echo $post->reactionsTotal('like'); // Count for 'like'
$user->reactTo($post, 'like');
$user->reactTo($post, 'rate', 4.5); // Rate between 1-5
if ($user->hasReactedTo($post, 'like')) {
$user->removeReaction($post, 'like');
}
$likedPosts = Post::whereReactedTo('like')->get();
$postsReactedByUser = Post::whereReactedToBy(auth()->user())->get();
$highRatedPosts = Post::whereReactedToBetween('rate', 4, 5)->get();
php artisan love:recount --queue-connection=redis
$post->reactionsTotal('like'); // Cached count
$post->reactionsAverage('rate'); // Average rating
ReactionType class or use the reactable array to define custom types:
protected $reactable = [
'upvote' => ['icon' => '↑', 'weight' => 1],
'downvote' => ['icon' => '↓', 'weight' => -1],
];
Route::post('/posts/{post}/react', function (Post $post) {
request()->user()->reactTo($post, request('type'), request('rate'));
return response()->json(['success' => true]);
});
return response()->json([
'reactions' => $post->reactions,
'counts' => $post->reactionsTotals,
]);
love_reactions table for reactant_id, reacter_id, and type.love:recount to avoid blocking requests:
'queue-connection' => env('QUEUE_CONNECTION', 'database'),
@foreach($post->reactable as $type)
<button onclick="react('{{ $type }}')">
{{ $post->reactionsTotal($type) }} {{ $type }}
</button>
@endforeach
function react(type) {
fetch(`/posts/${postId}/react`, {
method: 'POST',
body: JSON.stringify({ type }),
headers: { 'Content-Type': 'application/json' }
});
}
public function test_user_can_react_to_post()
{
$user = User::factory()->create();
$post = Post::factory()->create();
$user->reactTo($post, 'like');
$this->assertDatabaseHas('love_reactions', [
'reactant_id' => $post->id,
'reacter_id' => $user->id,
'type' => 'like',
]);
}
public function test_reaction_counts_are_cached()
{
$post = Post::factory()->create();
$user = User::factory()->create();
$user->reactTo($post, 'like');
$this->assertEquals(1, $post->fresh()->reactionsTotal('like'));
}
ReactionAggregate class to add custom metrics (e.g., "net sentiment"):
use Cog\Laravel\Love\ReactionAggregate;
class SentimentAggregate extends ReactionAggregate
{
public function calculate()
{
$positive = $this->reactions->where('type', 'like')->count();
$negative = $this->reactions->where('type', 'dislike')->count();
return $positive - $negative;
}
}
config/love.php:
'aggregates' => [
'sentiment' => \App\Aggregates\SentimentAggregate::class,
],
Foreign Key Constraints:
love_reactions, ensure foreign keys are handled:
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
// Delete records
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
removeReaction() instead of raw deletions.Rate Validation:
RATE_MIN and RATE_MAX (default: 1 and 5).try {
$user->reactTo($post, 'rate', 6); // Throws RateOutOfRange
} catch (\Cog\Contracts\Love\Reaction\Exceptions\RateOutOfRange $e) {
return response()->json(['error' => 'Rate must be between 1 and 5'], 400);
}
Queue Delays:
reactionsTotal, reactionsAverage) are updated asynchronously. For real-time needs:
sync queue connection during development:
'queue-connection' => 'sync',
php artisan love:recount
Model Caching:
reactionsTotal and reactionsCount. To refresh:
$post->refreshReactionsTotals();
fresh() to bypass cache:
$post->fresh()->reactionsTotal('like');
Migration Conflicts:
love_reactant_id column, drop the foreign key first:
Schema::table('love_reactions', function (Blueprint $table) {
$table->dropForeign(['reactant_id']);
});
Log Reactions:
config/database.php:
'log_queries' => true,
reactTo or removeReaction.Queue Monitoring:
love_reaction_aggregates_job and love_rebuild_aggregates_job in Laravel Horizon or queue workers:
php artisan queue:work --queue=reactant-aggregates
Common Issues:
reacter_id and reactant_id areHow can I help you explore Laravel packages today?