ghanem/rating
Ghanem Rating is a flexible Laravel package for adding ratings and reactions to any Eloquent model. It supports multiple rating types, average score calculations, user-specific ratings, and simple integration with Blade views and APIs. Ideal for star ratings, likes, and other feedback.
Install and publish
composer require ghanem/rating
On Laravel 5.5+ the Ghanem\Rating\RatingServiceProvider is auto-discovered. On older versions add it to config/app.php. Then publish the migration (and config, if present):
php artisan vendor:publish --provider="Ghanem\Rating\RatingServiceProvider"
php artisan migrate
Attach the trait to the model being rated
use Ghanem\Rating\Traits\Rating;
class Post extends Model
{
use Rating;
}
Where to look first
vendor/ghanem/rating/src/Traits/Rating.php — this is the entire public API surface; read it before writing anything (the package is small enough to audit in five minutes).rating vs value), the polymorphic rateable_type / rateable_id columns, and whether a unique index on (user_id, rateable_type, rateable_id) exists.config/rating.php if published — model bindings and table names are usually swappable here.First use case
The canonical flow is a logged-in user rating a post and the view rendering an average:
// Controller
public function rate(Post $post, Request $request)
{
$request->validate(['rating' => 'required|integer|min:1|max:5']);
$post->rating($request->rating, $request->user());
return back();
}
Then expose $post->averageRating() and $post->ratingCount() (or the equivalent accessors on your installed version) in the Blade view as a star widget. Because the trait is polymorphic, the same code works on Product, Article, Comment — no per-model table changes.
One rating per user, updated not duplicated. Treat rating as idempotent: a second submission should update the existing row rather than inflate the count. If the package's rating() method always inserts, wrap it:
$existing = $post->ratings()->where('user_id', $user->id)->first();
if ($existing) {
$existing->update(['rating' => $value]);
} else {
$post->rating($value, $user);
}
Better still, add a unique composite index and a dedicated updateRating() helper on your model so controllers never branch.
Explicit user argument over ambient auth. Pass $request->user() (or an explicit User instance) rather than relying on the package resolving auth()->user(). This keeps the same code path working for API token guards, impersonation, admin backfills, and seeders.
Avoid N+1 on index pages. A listing of 25 posts that each calls an aggregate method fires 25+ queries. Prefer:
$posts = Post::withCount('ratings')
->withAvg('ratings', 'rating')
->paginate(25);
This requires that the trait's relation is named consistently (typically ratings()); if not, define your own ratings() relation in the model and let the package methods delegate to it.
Sort by rating. Since averages are computed across rows, sort with a subquery join or a denormalized average_rating / ratings_count column on the parent table updated in a model observer on the Rating model. For anything beyond a hobby app, the denormalized column is the right call — it also makes filtering and pagination cheap.
API endpoint pattern.
Route::post('/posts/{post}/ratings', [RatingController::class, 'store'])
->middleware(['auth:sanctum', 'throttle:20,1']);
Return { average: 4.2, count: 31, mine: 5 } so the front end can update the widget without a refetch.
Authorization and eligibility. The package will happily let a user rate their own post. Add a policy or a guard clause:
abort_if($post->user_id === $request->user()->id, 403);
Testing. Use RefreshDatabase, create two users and a post, submit ratings, and assert the average and count. Because the surface is tiny, an integration test against the trait is more valuable than mocking anything. Factories for the Rating model let you seed realistic averages in feature tests.
Caching. Averages are read far more than written. Cache averageRating() per model with a short TTL and bust it in the Rating model's saved/deleted events.
rating(), averageRating(), ratingCount(), ratings()) are not a large, stable, documented API. Grep the installed source once and add a thin wrapper service or model helper around it so an upgrade touches one file, not fifty call sites.vendor:publish copies the migration with a timestamp; running it twice creates a second ratings table migration that will fail. Publish once, commit the file, and edit it in place thereafter.Relation::enforceMorphMap(), existing rows storing fully-qualified class names in rateable_type will no longer resolve. Either adopt the morph map from day one or write a data migration.users to ratings. Add ->onDelete('cascade') on the user_id foreign key, or clean up in a deleting observer on User.tinyint/int or decimal. Averages come back as floats or strings depending on the driver; cast explicitly ((float)) before formatting in Blade, and round at the display layer only.throttle middleware and a unique index; otherwise a determined user can skew an average with repeated submissions.auth()->id(), queue jobs, artisan commands, and unauthenticated API routes will all resolve null and either error or insert a rating with user_id = null. Always pass the user explicitly.$model->rating() collision. If your model already has a rating attribute or method, the trait will conflict. Rename your column or alias the trait method (use Rating { rating as rateModel; }).Rating model in config/rating.php, and override trait methods in your own Rateable trait if you need custom behavior — the source is small enough to own."ghanem/rating": "^1.0") rather than relying on *, since the sparse release history makes breaking changes hard to predict.DB::table('ratings')->where('rateable_id', $post->id)->get() tells you immediately whether the problem is duplicate rows, null users, or the aggregate method.How can I help you explore Laravel packages today?