mrshanebarron/rating
A Laravel rating package for attaching scores and reviews to Eloquent models. It supports configurable rating scales, user-submitted ratings, and aggregate calculations so you can display averages and review data in your app. Simple setup and model traits make it easy to integrate.
Install and pin. The package is at v1.0.1 with a config-filename fix in that release, so pin explicitly rather than floating:
composer require mrshanebarron/rating:^1.0.1
Publish the config first — not later. The v1.0.1 release note is "Fix config file naming (sb- prefix)." That means the published artifact is namespaced, likely config/sb-rating.php, with a matching publish tag (sb-rating-config or similar). Confirm both empirically before writing any application code:
php artisan vendor:publish --provider="MrShaneBarron\Rating\RatingServiceProvider"
# or, if a tag is documented:
php artisan vendor:publish --tag=sb-rating-config
Open config/sb-*.php and read it top to bottom. That file is your real API documentation for this package — it will name the rating model class and the min/max value range, which are the two things you need before anything else works.
Look here first, in order:
user model/key settings.vendor/mrshanebarron/rating/src/ — the model and the trait. This package's public surface is small enough to read in ten minutes, and with ~0 stars and a one-line changelog you should not assume undocumented behaviour.php artisan migrate; if it doesn't, check for a migration stub in the package and publish it under your own timestamp.First use case: rate a Post. The canonical flow is: add the package trait to your rateable model, then create a rating through the package's API.
// app/Models/Post.php
use MrShaneBarron\Rating\Traits\Rateable; // confirm exact namespace in vendor/
class Post extends Model
{
use Rateable;
}
$post->rate(5, $user); // confirm signature — may be rate($value, $user) or rate($user, $value)
$post->averageRating(); // returns a float/int per config
$post->ratings()->count();
Do this in a Tinker session or a single feature test before wiring any UI. If the trait name or method signature differs from the above, adjust — but the shape (trait on model, create rating, read aggregate) is what the package description commits to.
Put the trait on the rateable side, not the user side. Ratings are polymorphic: ratings.rateable_id + rateable_type point at Post, Product, Comment. The rating author is a separate column. So the trait goes on every model you want rated, and you typically do not touch your User model at all unless the package ships a reciprocal trait for "things I rated."
One rating per user per model is an application concern, not a package one. The package stores individual ratings; it does not reliably enforce uniqueness. Handle it explicitly:
$post->ratings()->updateOrCreate(
['user_id' => $user->id],
['score' => $request->integer('score')]
);
Or add a composite unique index in the published migration: unique(['rateable_type', 'rateable_id', 'user_id']). Decide up front whether re-rating updates or adds — the two produce very different averages, and the package won't decide for you.
Read aggregates in bulk for listings. The trap is this in a Blade loop:
@foreach ($posts as $post)
{{ $post->averageRating() }} {{-- N+1 --}}
@endforeach
For index pages, either eager-load and aggregate in PHP:
$posts = Post::with('ratings')->paginate(20);
// then $post->ratings->avg('score')
or store a denormalized average_rating / ratings_count column on the parent and update it in an observer on the rating model. The denormalized route is the one that survives growth; the package's own aggregate methods are fine for detail pages, not for feeds.
Treat the config as your extension point for the value range. If you need 1–10 stars instead of 1–5, change it in config/sb-*.php, not in your controllers. Then validate at the boundary against the same config key so the two can never disagree:
'score' => ['required', 'integer', 'min:'.config('sb-rating.min'), 'max:'.config('sb-rating.max')],
Swap the rating model via config, don't fork the package. Need a comment column on ratings, or a status for moderation? Point the config key at your own subclass of the package's rating model (adding your columns via your own migration on the same table), and override $fillable. This keeps you on the upgrade path.
Use morph maps. If you register Relation::enforceMorphMap(['post' => Post::class]), verify the package writes the alias rather than the FQCN — otherwise existing rows become unreadable the day you add the map. Add the map before seeding production ratings, or not at all.
Integration tip: wrap writes in a domain action, not a controller. A RatePost action that validates the score, upserts the rating, and fires your own PostRated event gives you one place to change when the package's API shifts — which, at v1.0.x with a single maintainer, is a reasonable bet.
The config filename change is the sharpest edge in this package. If you were on v1.0.0, the old published file still sits in config/ under its old name. v1.0.1 looks for sb-*. The old file is then dead weight that shadows nothing but misleads everyone — a developer edits it, sees no effect, and loses an hour. On upgrade: publish the new file, diff it against the old, port customisations, delete the orphan, then grep -r "config('sb" app/ to catch key references. Also check whether the publish tag changed, because vendor:publish --tag=... in CI will fail hard on an unknown tag.
Always clear config after touching this package. php artisan config:clear locally, and ensure your deploy runs config:cache after composer install. Config caching is what turns a filename mismatch into a bug that reproduces in production but not in staging.
Range violations fail silently-ish. If the config range is 1–5 and someone posts 10, the package may store it and skew every average with no error. Validate before write; don't rely on the package to reject.
Average type and empty state. averageRating() on a model with zero ratings typically returns 0, null, or a division-by-zero depending on implementation — check all three cases. Render 0 vs — deliberately, and round at the display layer (number_format($avg, 1)), never in storage.
Soft deletes and orphaned ratings. Ratings are polymorphic, so there is no foreign key cascade on rateable_id. Deleting a Post leaves ratings behind; a User deletion leaves ratings attributed to nobody and can null out your author column. Add a deleting observer on each rateable model that cleans ratings(), and decide whether soft-deleted ratings count toward averages (they will unless you scope them out).
Guests and the author column. If your app allows unauthenticated rating (session- or IP-based), the package's user column is nullable-or-not at the migration level. Check the published migration before assuming; a NOT NULL user_id will throw on your first guest rating in production.
Octane and long-running workers. The service provider registers config at boot; that's fine. But if you cache aggregates in a singleton or static, Octane will leak them between requests. Keep aggregate state in the database or the request, never in a container singleton.
Debugging checklist, in order: (1) Is the published config actually the sb- file? (2) config('sb-...') — does the key resolve, or null? (3) Did you config:clear? (4) Does the trait's namespace match what's in vendor/? (5) Does the ratings table exist with the columns the model expects (rateable_type, rateable_id, author, score)?
Extension points worth knowing: the config-declared rating model (subclass it for extra columns), the trait (add your own scopeTopRated alongside it), and the config value range. Those three cover almost every customisation request without forking. Everything else — uniqueness, moderation, denormalization, caching — belongs in your application, and putting it there is what keeps a 0-star dependency from becoming a 0-star liability.
Pin the minor and read the changelog on every bump. A config rename shipped as a patch tells you the maintainer treats the config surface as non-public. Add a one-line smoke test that asserts a non-default config value reads back correctly; it is the cheapest guard you will ever write against this class of churn.
How can I help you explore Laravel packages today?