kosmoskosmos/rating
Laravel package for adding star ratings to Eloquent models. Supports polymorphic ratings, configurable scales, average score calculations, and simple helpers for displaying rating summaries in your app.
1. Confirm compatibility before installing. The package targets Laravel Nova and Laravel 9/10-era PHP (8.1/8.2). Check composer.json inside the package for illuminate/*, laravel/nova, and php constraints against your project. Nova 5 / PHP 8.3 compatibility is not guaranteed given the October 2023 last release.
composer require kosmoskosmos/rating
2. Publish and run the migration. The package ships a ratings table migration (polymorphic: rateable_type, rateable_id, user_id, rating/score). Publish via the service provider:
php artisan vendor:publish --provider="Kosmos\Rating\RatingServiceProvider"
php artisan migrate
Verify the actual provider FQCN first with php artisan vendor:publish (interactive list) or by reading vendor/kosmoskosmos/rating/src. The Composer name (kosmoskosmos/rating) and repo name (nova-rating) differ, so don't assume the namespace from either.
3. Add the trait to your rateable model.
use Kosmos\Rating\Traits\Rateable; // confirm namespace
class Product extends Model
{
use Rateable;
}
4. Register the Nova field on the resource.
use Kosmos\Rating\Rating; // confirm namespace
public function fields(NovaRequest $request)
{
return [
Rating::make('Rating'),
];
}
Where to look first: vendor/kosmoskosmos/rating/src/ — the trait (defines rate(), ratings(), aggregate helpers), the Nova field class (defines index/detail/form behavior), and config/rating.php if present (table name, user model, storage column).
First real use case: per-user star ratings on a Product. A logged-in user submits 1–5 stars in the Nova detail form or via your own endpoint; the model exposes an average for index columns and API responses. With 3 stars and 0 dependents, scope your first integration to a single low-risk resource and validate the output before spreading it.
Write path — rate once, update thereafter. Duplicate ratings are the most common failure mode. Wrap writes so a user can only ever hold one rating per rateable:
$product->ratings()->updateOrCreate(
['user_id' => auth()->id()],
['rating' => $request->integer('rating')]
);
Or use the trait's own method if it provides one ($product->rate($value) / $product->rate($value, $user)) — read the trait source to see whether it uses updateOrCreate internally or blindly create()s. If it creates, add a unique DB index and handle the collision.
Read path — never call averageRating() in a loop. On a Nova index with 50 rows that is 50 aggregate queries. Use constrained eager loading with aliases and point the field at the alias:
public function indexQuery(NovaRequest $request, $query)
{
return $query
->withAvg('ratings', 'rating')
->withCount('ratings');
}
Rating::make('Avg Rating', 'ratings_avg_rating')
->sortable(), // only if it maps to a real/aliased column
Rating::make('Votes', 'ratings_count')->sortable(),
Sorting by rating. orderBy('ratings_avg_rating') works against the withAvg alias in MySQL/Postgres; make the Nova field sortable() and confirm the generated ORDER BY references the alias, not a nonexistent rating column.
Form vs. display. Typical split: a writable Rating::make('Rating') on the update/create forms, and read-only computed fields (ratings_avg_rating, ratings_count) on index/detail. If the field renders stars on index, confirm it degrades gracefully at null (no ratings yet) rather than showing zero stars as a real score.
API / non-Nova consumption. The trait is the real product; the Nova field is a thin view. Expose ratings through a controller and a RatingResource so mobile/JS clients use the same write path as Nova. Return avg rounded at the presentation layer, not in the DB.
Caching aggregates. Averages are read far more than written. Cache avg and count per rateable with a short TTL or bust them in a model observer on the Rating model (saved/deleted). This is the cheapest way to make the package viable on listing pages.
Composition with other fields. Because the storage is polymorphic, the same ratings table can serve Product, Post, and User without schema changes. Adopt Relation::morphMap() early so rateable_type stores stable aliases ('product') instead of FQCNs.
src/ before adopting; you can realistically fork or copy it into app/ and own it.php artisan vendor:publish and grep -r "namespace" vendor/kosmoskosmos/rating/src before writing imports.Relation::morphMap() before the first row is written, not after.tinyint, per-user values are integers and your average is a float — round only at render time. If the column is decimal, make sure the field's rules() constrain input to your scale, otherwise Nova accepts 4.75 into a decimal(3,1) and truncation surprises you.null from withAvg, which often renders as 0 stars and reads as "rated 0". Use displayUsing() to render "—" or "No ratings" for null.user_id = null, the unique index still allows unlimited NULL rows in MySQL — duplicates are permitted. Either enforce auth or add a session-key column.averageRating() in a Blade/Nova loop. Audit every call site with DB::listen() (or Telescope) in a staging index request before shipping.Rating model and point the config at it, or fork outright.ratings()) matches what the field and your aggregates reference; a renamed relation silently returns null averages rather than erroring."kosmoskosmos/rating": "1.0.*" (or whatever the exact tag is) in composer.json rather than a floating constraint. With no maintainer activity, an unpinned composer update cannot be relied upon to be a no-op.How can I help you explore Laravel packages today?