NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Reviews for Laravel

Ratings & aggregates

HasReviews gives every subject its relations and live aggregates. Aggregates count approved, top-level reviews only — pending, rejected and owner responses never move the score:

$restaurant->reviews;                 // MorphMany (all rows, responses included)
$restaurant->topLevelReviews;         // MorphMany (owner responses excluded)
$restaurant->approvedReviews;         // approved, top-level only
$restaurant->averageRating();         // ?float, null when none
$restaurant->reviewsCount();          // int (all top-level, any status)
$restaurant->approvedReviewsCount();  // int (approved)
$restaurant->ratingDistribution();    // [5 => 120, 4 => 80, ...]
$restaurant->ratingSummary();         // RatingSummary DTO
On the model (HasReviews)On the facadeCounts
averageRating()for($x)->average()Average of approved, top-level, rated reviews; null when none.
approvedReviewsCount()for($x)->count()Approved, top-level reviews (text-only included).
ratingDistribution()for($x)->distribution()Approved, rated reviews per rating value, highest first.
ratingSummary()for($x)->summary()All of the above plus photo counts, as a RatingSummary.
reviewsCount()—Every top-level review, whatever its status.
—for($x)->photoCount()Photos across approved, top-level reviews.
—for($x)->reviewsWithPhotos()Approved, top-level reviews with at least one photo.

The rating summary

ratingSummary() returns a RatingSummary DTO — everything a product page needs in one object, with toArray() for JSON responses:

$summary = $restaurant->ratingSummary();

$summary->average;            // 4.6 (null when there are no rated reviews)
$summary->count;              // 320
$summary->distribution;       // [5 => 210, 4 => 80, 3 => 20, 2 => 6, 1 => 4]
$summary->photoCount;         // 57
$summary->reviewsWithPhotos;  // 41

return response()->json($summary->toArray());
// ['average' => 4.6, 'count' => 320, 'distribution' => [...],
//  'photo_count' => 57, 'reviews_with_photos' => 41]
  • average ignores text-only reviews; count includes them.
  • distribution is keyed by rating value, highest first, and only lists values that have at least one review.
  • photoCount and reviewsWithPhotos are 0 when photos are disabled.

On the facade

The same aggregates are available for any model through Reviews::for(), with or without the trait — the trait methods call it too:

use RoundlyConsulting\Reviews\Facades\Reviews;

$reviews = Reviews::for($restaurant);

$reviews->average();                  // ?float, null when none
$reviews->count();                    // int (approved)
$reviews->distribution();             // [5 => 120, 4 => 80, ...]
$reviews->photoCount();               // total photos on approved reviews
$reviews->reviewsWithPhotos();        // approved reviews that carry a photo
$reviews->summary();                  // RatingSummary DTO — all of the above

Each call runs a query. For high-traffic listings, cache the count and average on the subject’s own table — see Cached aggregates.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.