LaravelPackages.net
Acme Inc.
Toggle sidebar
ghanem/rating

Rating system for Laravel

15.631
86
v2.1.1
About ghanem/rating

ghanem/rating is a Laravel package for rating system for laravel. It currently has 86 GitHub stars and 15.631 downloads on Packagist (latest version v2.1.1). Install it with composer require ghanem/rating. Discover more Laravel packages by ghanem or browse all Laravel packages to compare alternatives.

Last updated

Laravel Rating

Latest Stable Version License Total Downloads

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 your composer.json says "ghanem/rating": "^12.0", change it to "^2.1" and run composer update ghanem/rating. No code changes are needed — v2.1.0 is 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;
}

Ratingable is a trait, not an interface. Put it in use inside the class body — never in implements. class Post extends Model implements Ratingable fails 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\RatingCreated
  • Ghanem\Rating\Events\RatingUpdated
  • Ghanem\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 |

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.

Become a Sponsor

Comments