ghanem / rating
Rating system for Laravel
Requires
- php: ^8.0
- illuminate/database: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.0|^10.0|^11.0|^12.0
README
Laravel Rating
Rating system for Laravel 8, 9, 10, 11, 12 & 13.
Using Filament? See ghanem/rating-filament — a star input field, a sortable average-rating table column, an infolist entry and a review moderation relation manager for Filament 4 & 5.
Installation
composer require ghanem/rating
Upgrading from
V12.0? That tag was a mis-tag: it read as "Laravel 12 support" but registered on Packagist as major version 12, so it outranked every 2.x release. It has been removed. If yourcomposer.jsonsays"ghanem/rating": "^12.0", change it to"^2.1"and runcomposer update ghanem/rating. No code changes are needed —v2.1.0is the same line, plus Laravel 13 support and the migration publishing fixes.
The package uses Laravel's auto-discovery, so no need to manually register the service provider.
Getting started
Publish and run the migration:
php artisan vendor:publish --provider="Ghanem\Rating\RatingServiceProvider"
php artisan migrate
Optionally publish the config file:
php artisan vendor:publish --tag=rating-config
Usage
Setup a Model
Add the Ratingable trait to any model you want to be ratable:
use Ghanem\Rating\Traits\Ratingable; class Post extends Model { use Ratingable; }
Ratingableis a trait, not an interface. Put it inuseinside the class body — never inimplements.class Post extends Model implements Ratingablefails with "cannot implement Ratingable - it is not an interface".
Add the CanRate trait to the author model:
use Ghanem\Rating\Traits\CanRate; class User extends Model { use CanRate; }
Create a rating
// From the ratable model $rating = $post->rating(['rating' => 5], $user); // From the author model $rating = $user->rate($post, ['rating' => 5]);
Create or update a unique rating
Only one rating per author per model:
$rating = $post->ratingUnique(['rating' => 5], $user); // Or from the author $rating = $user->rateUnique($post, ['rating' => 5]);
Update a rating
$rating = $post->updateRating($ratingId, ['rating' => 3]);
Delete a rating
$post->deleteRating($ratingId);
Rating with review body
$post->rating([ 'rating' => 5, 'body' => 'Great article!', ], $user);
Scoped ratings (rate different aspects)
$restaurant->rating(['rating' => 5, 'type' => 'food'], $user); $restaurant->rating(['rating' => 3, 'type' => 'service'], $user); $restaurant->avgRating('food'); // 5.0 $restaurant->avgRating('service'); // 3.0 $restaurant->avgRating(); // 4.0 (all types)
Weighted ratings
$post->rating(['rating' => 5, 'weight' => 2], $verifiedUser); $post->rating(['rating' => 3, 'weight' => 1], $regularUser); $post->weightedAvgRating(); // 4.33
Aggregates
All aggregate methods accept an optional $type parameter for scoped ratings:
$post->avgRating() // average rating $post->sumRating() // sum of all ratings $post->countRatings() // total count $post->countPositive() // count where rating > 0 $post->countNegative() // count where rating < 0 $post->ratingPercent() // percentage (default max: 5) $post->ratingPercent(10) // percentage with custom max $post->weightedAvgRating() // weighted average
All available as attributes too:
$post->avgRating $post->sumRating $post->countRatings $post->countPositive $post->countNegative $post->ratingPercent $post->weightedAvgRating
Author queries (CanRate)
$user->hasRated($post); // bool $user->getRating($post); // Rating|null $user->averageGivenRating(); // float $user->totalGivenRatings(); // int $user->ratings; // all ratings given
Check if rated
$post->isRatedBy($user); // bool $post->isRatedBy($user, 'food'); // bool (scoped)
Query scopes
// Eager load rating aggregates Post::withAvgRating()->get(); Post::withSumRating()->get(); Post::withCountRatings()->get(); // Order by ratings Post::orderByAvgRating()->get(); // desc by default Post::orderByAvgRating('asc')->get(); Post::orderBySumRating()->get(); Post::orderByCountRatings()->get(); // Filter by minimum rating Post::minAvgRating(3.5)->get(); Post::minSumRating(10)->get(); // Scoped by type Post::withAvgRating('food')->get(); Post::orderByAvgRating('desc', 'food')->get();
Displaying ratings in Blade
The package is storage-only — it ships no views or assets, so you stay in control of your markup. Here is a complete 5-star setup with no JavaScript and no front-end dependencies.
1. Read-only stars
ratingPercent() already returns the average as a percentage of the maximum, which is
exactly what a CSS clip needs. Fractional averages (3.7 stars) render correctly with no
extra work.
{{-- resources/views/components/stars.blade.php --}} @props(['percent' => 0]) <span {{ $attributes->merge(['class' => 'stars']) }} style="--rating: {{ $percent }}%"> ★★★★★ </span>
.stars { position: relative; display: inline-block; color: #d1d5db; letter-spacing: 2px; white-space: nowrap; } .stars::before { content: '★★★★★'; position: absolute; top: 0; left: 0; width: var(--rating); overflow: hidden; color: #f59e0b; letter-spacing: 2px; }
<x-stars :percent="$post->ratingPercent()" /> <span>{{ number_format($post->avgRating(), 1) }} out of 5 ({{ $post->countRatings() }})</span>
For a 10-point scale, pass the max: $post->ratingPercent(10).
2. An interactive rating form
Radio inputs in reverse order, so the CSS sibling selector can highlight the hovered star and every star before it. Accessible and keyboard-operable, because it is a real radio group.
<form method="POST" action="{{ route('posts.rate', $post) }}"> @csrf <fieldset class="rating-input"> <legend>Your rating</legend> @foreach (range(5, 1) as $value) <input type="radio" id="star-{{ $value }}" name="rating" value="{{ $value }}" {{ auth()->user()?->getRating($post)?->rating == $value ? 'checked' : '' }} > <label for="star-{{ $value }}" title="{{ $value }} stars">★</label> @endforeach </fieldset> <textarea name="body" placeholder="Leave a review (optional)"></textarea> <button type="submit">Submit</button> </form>
.rating-input { display: inline-flex; flex-direction: row-reverse; /* lets `~` reach the stars to the left */ border: 0; } .rating-input input { position: absolute; opacity: 0; /* hidden from sight, still focusable */ } .rating-input label { cursor: pointer; font-size: 1.75rem; color: #d1d5db; } .rating-input input:checked ~ label, .rating-input label:hover, .rating-input label:hover ~ label { color: #f59e0b; } .rating-input input:focus-visible + label { outline: 2px solid #2563eb; }
3. Route and controller
// routes/web.php Route::post('posts/{post}/rate', [RatingController::class, 'store']) ->middleware('auth') ->name('posts.rate');
class RatingController extends Controller { public function store(Request $request, Post $post) { $data = $request->validate([ 'rating' => ['required', 'integer', 'min:1', 'max:5'], 'body' => ['nullable', 'string', 'max:2000'], ]); // rateUnique() updates the user's existing rating instead of adding a second one $request->user()->rateUnique($post, $data); return back()->with('status', 'Thanks for rating!'); } }
Validate in the request as well as configuring config/rating.php. The config bounds
throw InvalidRatingException, which surfaces as a 500; request validation gives the
user a normal field error instead.
4. Listing many rated models
Calling $post->avgRating() inside a loop runs one aggregate query per row. Load
the aggregates with the query instead:
$posts = Post::withAvgRating()->withCountRatings()->paginate();
@foreach ($posts as $post) {{-- read the eager-loaded aliases, not the accessors --}} <x-stars :percent="($post->ratings_avg_rating / 5) * 100" /> <span>{{ $post->ratings_count }} ratings</span> @endforeach
withAvgRating() selects a ratings_avg_rating alias and withCountRatings() selects
ratings_count. Both are plain columns on the result, so sorting and filtering happen
in SQL — see Query scopes.
Validation
Configure rating bounds in config/rating.php:
return [ 'min' => 1, 'max' => 5, 'allow_negative' => false, ];
Invalid ratings throw Ghanem\Rating\Exceptions\InvalidRatingException.
Events
The package fires events on rating lifecycle:
Ghanem\Rating\Events\RatingCreatedGhanem\Rating\Events\RatingUpdatedGhanem\Rating\Events\RatingDeleted
Each event has a public $rating property with the Rating model.
Filament admin panel
ghanem/rating-filament (Packagist) adds Filament 4 & 5 components on top of this package:
composer require ghanem/rating-filament
| Component | Purpose |
|---|---|
RatingInput |
Clickable star picker for forms, with validation bounds read from config/rating.php |
RatingColumn |
Sortable average-rating column, backed by withAvgRating() so it does not N+1 |
RatingEntry |
Read-only stars for infolists |
RatingsRelationManager |
Moderate the ratings and reviews a record received |
Related packages
- ghanem/rating-filament — Filament 4 & 5 admin components for this package
- ghanem/friendship — friendships, requests and blocks for Eloquent models
- ghanem/friendship-filament — Filament admin panel for
ghanem/friendship
Testing
composer test
Credits
This package began life in 2015 as an MIT-licensed package by DraperStudio / PackageBackup, whose original repository is no longer published. It has been maintained by GAIT ever since, across Laravel 5 through 13. The original copyright notice is retained in LICENSE.
