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.
composer require ghanem/ratingUpgrading 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.
Publish and run the migration:
php artisan vendor:publish --provider="Ghanem\Rating\RatingServiceProvider"
php artisan migrateOptionally publish the config file:
php artisan vendor:publish --tag=rating-configAdd 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;
}// From the ratable model
$rating = $post->rating(['rating' => 5], $user);
// From the author model
$rating = $user->rate($post, ['rating' => 5]);Only one rating per author per model:
$rating = $post->ratingUnique(['rating' => 5], $user);
// Or from the author
$rating = $user->rateUnique($post, ['rating' => 5]);$rating = $post->updateRating($ratingId, ['rating' => 3]);$post->deleteRating($ratingId);$post->rating([
'rating' => 5,
'body' => 'Great article!',
], $user);$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)$post->rating(['rating' => 5, 'weight' => 2], $verifiedUser);
$post->rating(['rating' => 3, 'weight' => 1], $regularUser);
$post->weightedAvgRating(); // 4.33All 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 averageAll available as attributes too:
$post->avgRating
$post->sumRating
$post->countRatings
$post->countPositive
$post->countNegative
$post->ratingPercent
$post->weightedAvgRating$user->hasRated($post); // bool
$user->getRating($post); // Rating|null
$user->averageGivenRating(); // float
$user->totalGivenRatings(); // int
$user->ratings; // all ratings given$post->isRatedBy($user); // bool
$post->isRatedBy($user, 'food'); // bool (scoped)// 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();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.
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).
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;
}// 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.
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>
@endforeachwithAvgRating() 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.
Configure rating bounds in config/rating.php:
return [
'min' => 1,
'max' => 5,
'allow_negative' => false,
];Invalid ratings throw Ghanem\Rating\Exceptions\InvalidRatingException.
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.
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 |
- 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
composer testThis 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.
