Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Vote Laravel Package

overtrue/laravel-vote

Add voting and rating to Laravel Eloquent models with overtrue/laravel-vote. It provides polymorphic vote relationships, upvote/downvote support, vote totals, and simple traits to attach voting to users and any model, making community feedback and ranking easy to implement.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements (as of 4.0.0): PHP 8.2+, Laravel 10.x / 11.x / 12.x / 13.x. If you are still on Laravel 9 you must stay on 3.4.x — 4.0.0 is a hard floor bump with no compatibility shim.

Install:

composer require overtrue/laravel-vote:^4.0

Publish and run the migration (the package ships a votes table migration — check the README for the exact publish tag, it is typically vote-migrations):

php artisan vendor:publish --tag=vote-migrations
php artisan migrate

Wire the traits. This is the only real setup step:

use Overtrue\LaravelVote\Traits\Voter;

class User extends Authenticatable
{
    use Voter;
}

use Overtrue\LaravelVote\Traits\Votable;

class Post extends Model
{
    use Votable;
}

First use case — an upvote button:

$user->vote($post);              // upvote (default)
$user2->vote($post, 'down');     // downvote
$user->cancelVote($post);        // remove

$post->hasVotedBy($user);        // bool
$post->votes()->count();         // total
$post->voters;                   // collection of User models

If that snippet works end-to-end, the package is installed correctly and everything else is query hygiene.


Implementation Patterns

Idempotent toggle endpoint. The single most common pattern is a toggle route, because the frontend should never have to know current state:

public function vote(Request $request, Post $post)
{
    $user = $request->user();

    if ($user->hasVoted($post)) {
        $user->cancelVote($post);
        return ['voted' => false, 'count' => $post->votes()->count()];
    }

    $user->vote($post, $request->input('type', 'up'));
    return ['voted' => true, 'count' => $post->votes()->count()];
}

Wrap it in a DB transaction if you also maintain a denormalised votes_count column, otherwise concurrent double-submits can desync the counter from the rows.

Separate up/down semantics. Use distinct buttons instead of a toggle when downvotes exist — vote($post, 'up') and vote($post, 'down') overwrite rather than stack, so a user switching from up to down should call cancelVote() then vote(..., 'down') (or just vote($post, 'down') if you accept overwrite semantics — verify against your version, this is the one behaviour worth writing a test for).

Eager-load the voters list. Listing who liked a post is the classic N+1 trap:

$posts = Post::withCount('votes')->with('voters')->paginate(20);

Then in the view use the loaded relation, never $post->hasVotedBy(auth()->user()) inside a loop — that fires one query per row. For "did I vote", select the current user's votes once:

$myVotes = $user->votedItems(Post::class)->pluck('id')->flip();

Reverse lookups work too. $user->votedItems(Post::class) and $user->votes() (all votes cast by the user) are useful for a "your activity" page and for GDPR-style export/delete flows.

Multiple votable types. Anything can be votable — comments, replies, reviews. Just add the Votable trait. Use Laravel's morph map so the DB stores stable aliases instead of FQCNs:

// AppServiceProvider::boot()
Relation::enforceMorphMap([
    'post'    => \App\Models\Post::class,
    'comment' => \App\Models\Comment::class,
]);

Do this before shipping to production, not after — see gotchas.

Testing. The trait-based API is trivially fakeable, no mocking library needed:

$user = User::factory()->create();
$post = Post::factory()->create();

$this->actingAs($user)->post("/posts/{$post->id}/vote")->assertOk();
$this->assertTrue($user->fresh()->hasVoted($post));

Add a test that a second vote() call does not create a duplicate row — that is the failure mode that silently inflates counts.

Blade / Inertia. Expose has_voted and votes_count in your API resource; don't let the view call the trait directly. It keeps the query plan visible and lets you swap the voting backend later.


Gotchas and Tips

Unique index is your safety net. The package migration should give you a unique constraint on (user_id, votable_id, votable_type). If you hand-rolled or modified the migration, add it — without it, retries and double-clicks create duplicate votes that make votes()->count() wrong forever and are painful to clean up retroactively.

Never change the morph map once data exists. Switching between FQCN and aliases (or renaming an alias) orphans every existing vote row, because votable_type stores a string. If you must migrate, write a one-off UPDATE votes SET votable_type = ... in the same deploy.

cancelVote() vs unvote() — check your version. Naming shifted across 2.x/3.x. Grep the trait in vendor/overtrue/laravel-vote/src/Traits/ rather than trusting a blog post; it is a 30-second read and saves a confusing BadMethodCallException.

Counting at scale. $post->votes()->count() is a full aggregate every call. For feeds, use withCount('votes'), or maintain a votes_count column updated in the same transaction as the vote. Don't reach for votes()->count() inside a loop under any circumstances.

Upvotes vs downvotes are a sign, not a row type. The single votes column stores +1/-1. To get a net score you sum, to get "likes only" you filter. Check which convention your version writes before writing raw SQL — mixing the two produces a score of zero on popular posts, which is a very confusing bug report.

Soft deletes leave orphan votes. If votable models are soft-deletable, votes rows survive the delete. Either hard-delete votes in an observer or filter them out of counts with a join. Same applies to deleted users — decide policy up front.

Upgrading 3.4 → 4.0. There are no documented API changes; the whole major is platform requirements (PHP 8.2, drop Laravel 9, add Laravel 13). So the upgrade is: bump PHP on the runtime and CI, run composer update overtrue/laravel-vote, re-run your vote feature tests. Projects pinned to Laravel 9 must stay on ^3.4 — set that constraint explicitly so Composer doesn't resolve into a broken state.

Watch for stray Pint churn. 4.0.0 reformatted the package with Laravel Pint and widened orchestra/testbench / phpunit / mockery constraints. If you have a composer.lock with tight dev constraints, run composer update --with-all-dependencies for the dev group, and don't be alarmed by style-only diffs if you've vendored or published any package files.

Extension point: model events. Vote records are Eloquent models, so Vote::created(...) observers are the cleanest hook for notifications ("X liked your post"), analytics, or counter-cache updates — cleaner than subclassing the traits, which is fragile across minor releases.

Debugging a wrong count. In order: (1) duplicate rows — select user_id, votable_id, votable_type, count(*) ... having count(*) > 1; (2) orphaned rows from soft deletes; (3) mixed morph types from a changed morph map; (4) a stale denormalised counter. Almost every "the vote count is wrong" bug is one of those four, not the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator