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 facade | Counts |
|---|---|---|
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 aboveEach 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 cryptoBy 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.