Skip to main content
Reviews allow customers to rate and review products. Shopper provides a flexible review system with approval workflows, rating calculations, and polymorphic relationships.

Model

The model used is Shopper\Core\Models\Review. It implements Shopper\Core\Models\Contracts\Review. Unlike products or brands, the Review model is not configurable via config/shopper/models.php and has no admin model wrapper. Products gain review capabilities through the InteractsWithReviews trait, which implements the Shopper\Core\Contracts\HasReviews interface. This is already applied to the Product model.

Database Schema

ColumnTypeNullableDefaultDescription
idbigintnoautoPrimary key
ratingintegerno-Rating value (typically 1-5)
titletextyesnullReview title
contenttextyesnullReview content
is_recommendedbooleannofalseCustomer recommends product
approvedbooleannofalseReview approval status
reviewrateable_idbigintno-Polymorphic relation ID (product)
reviewrateable_typestringno-Polymorphic relation type
author_idbigintno-Polymorphic relation ID (user)
author_typestringno-Polymorphic relation type
created_attimestampyesnullCreation timestamp
updated_attimestampyesnullLast update timestamp

Relationships

Reviewrateable (Product)

// Get the reviewed entity (usually a product)
$review->reviewrateable; // Product model

Author (User)

// Get the review author
$review->author; // User model

HasReviews Contract

Products implement the HasReviews contract via the InteractsWithReviews trait:
use Shopper\Core\Contracts\HasReviews;
use Shopper\Core\Models\Traits\InteractsWithReviews;

class Product extends Model implements HasReviews
{
    use InteractsWithReviews;
}

Rating Methods

Getting Ratings

// Get all ratings for a product
$product->ratings; // Collection of Review models

// Get all ratings with sorting
$product->getAllRatings($product->id, 'desc');

// Get only approved ratings
$product->getApprovedRatings($product->id);

// Get pending (not approved) ratings
$product->getNotApprovedRatings($product->id);

// Get recent ratings
$product->getRecentRatings($product->id, 5);

// Get recent ratings by user
$product->getRecentUserRatings($userId, 5, approved: true);

Rating Counts

// Count all ratings
$product->countRating(); // Returns integer

// Get rating sum
$product->sumRating(); // Collection with sum

Average Ratings

// Get average rating
$product->averageRating(); // Collection with average

// Get average rating rounded to 1 decimal
$product->averageRating(round: 1); // e.g., 4.5

// Get average rating of approved reviews only
$product->averageRating(round: 1, onlyApproved: true);

Rating Percentage

// Get percentage of 5-star ratings
$product->ratingPercent(5); // e.g., 85.5

// Useful for displaying rating distribution
$distribution = [
    5 => $product->ratingPercent(5),
    4 => $product->ratingPercent(4),
    3 => $product->ratingPercent(3),
    2 => $product->ratingPercent(2),
    1 => $product->ratingPercent(1),
];

Creating Reviews

Via Product Model

use Shopper\Models\Product;

$product = Product::query()->find($id);

// Create a review
$review = $product->rating([
    'rating' => 5,
    'title' => 'Excellent product!',
    'content' => 'This product exceeded my expectations.',
    'is_recommended' => true,
    'approved' => false, // Pending approval
], $user);

Via Review Model

use Shopper\Core\Models\Review;

$review = new Review;
$review = $review->createRating($product, [
    'rating' => 4,
    'title' => 'Good quality',
    'content' => 'Happy with my purchase.',
    'is_recommended' => true,
], $user);

Direct Creation

$review = Review::query()->create([
    'rating' => 5,
    'title' => 'Love it!',
    'content' => 'Best purchase I made.',
    'is_recommended' => true,
    'approved' => false,
    'reviewrateable_id' => $product->id,
    'reviewrateable_type' => get_class($product),
    'author_id' => $user->id,
    'author_type' => get_class($user),
]);

Updating Reviews

// Via product model
$product->updateRating($reviewId, [
    'rating' => 4,
    'content' => 'Updated review content.',
]);

// Via review model
$review = new Review;
$review->updateRating($reviewId, [
    'title' => 'Updated title',
    'content' => 'Updated content',
]);

Approval Management

// Approve a review
$review->updatedApproved(true);

// Unapprove a review
$review->updatedApproved(false);

// Direct update
$review->update(['approved' => true]);

Deleting Reviews

// Via product model
$product->deleteRating($reviewId);

// Via review model
$review = new Review;
$review->deleteRating($reviewId);

// Direct delete
Review::query()->find($reviewId)->delete();

Retrieving Reviews

use Shopper\Core\Models\Review;

// Get all reviews
$reviews = Review::query()
    ->with(['author', 'reviewrateable'])
    ->latest()
    ->get();

// Get pending reviews
$pending = Review::query()
    ->where('approved', false)
    ->with('author')
    ->get();

// Get approved reviews for a product
$productReviews = Review::query()
    ->where('reviewrateable_id', $productId)
    ->where('reviewrateable_type', Product::class)
    ->where('approved', true)
    ->with('author')
    ->latest()
    ->paginate(10);

// Get reviews by user
$userReviews = Review::query()
    ->where('author_id', $userId)
    ->where('author_type', User::class)
    ->with('reviewrateable')
    ->get();

Configuration

Disabling Reviews

If your store doesn’t need product reviews, disable the feature in config/shopper/features.php:
use Shopper\Enum\FeatureState;

return [
    'review' => FeatureState::Disabled,
];

Permissions

The admin panel generates five permissions for review management:
PermissionDescription
browse_reviewsView the reviews list
read_reviewsView a single review
add_reviewsCreate new reviews
edit_reviewsEdit existing reviews
delete_reviewsDelete reviews

Components

To customize the admin UI for review management:
php artisan shopper:component:publish review
Creates config/shopper/components/review.php:
use Shopper\Livewire;

return [
    'pages' => [
        'review-index' => Livewire\Pages\Reviews\Index::class,
    ],
    'components' => [
        'slide-overs.review-detail' => Livewire\SlideOvers\ReviewDetail::class,
    ],
];

Storefront Example

Review Display

namespace App\Http\Controllers;

use Shopper\Models\Product;

class ProductController extends Controller
{
    public function show(string $slug)
    {
        $product = Product::findBySlug($slug);

        // Get approved reviews
        $reviews = $product->ratings()
            ->where('approved', true)
            ->with('author')
            ->latest()
            ->paginate(10);

        // Get rating stats
        $averageRating = $product->averageRating(1, onlyApproved: true)->first();
        $reviewCount = $product->ratings()->where('approved', true)->count();

        // Rating distribution
        $distribution = [];
        for ($i = 5; $i >= 1; $i--) {
            $count = $product->ratings()
                ->where('approved', true)
                ->where('rating', $i)
                ->count();
            $distribution[$i] = [
                'count' => $count,
                'percentage' => $reviewCount > 0 ? ($count / $reviewCount) * 100 : 0,
            ];
        }

        return view('products.show', compact(
            'product',
            'reviews',
            'averageRating',
            'reviewCount',
            'distribution'
        ));
    }
}

Review Submission

namespace App\Http\Controllers;

use Shopper\Models\Product;

class ReviewController extends Controller
{
    public function store(Request $request, Product $product)
    {
        $validated = $request->validate([
            'rating' => 'required|integer|min:1|max:5',
            'title' => 'nullable|string|max:255',
            'content' => 'nullable|string|max:2000',
            'is_recommended' => 'boolean',
        ]);

        // Check if user already reviewed this product
        $existingReview = $product->ratings()
            ->where('author_id', auth()->id())
            ->where('author_type', get_class(auth()->user()))
            ->exists();

        if ($existingReview) {
            return back()->with('error', 'You have already reviewed this product.');
        }

        // Create review (pending approval)
        $product->rating([
            ...$validated,
            'approved' => false,
        ], auth()->user());

        return back()->with('success', 'Thank you! Your review is pending approval.');
    }
}

Review Management (Admin)

namespace App\Http\Controllers\Admin;

use Shopper\Core\Models\Review;

class ReviewController extends Controller
{
    public function index()
    {
        $reviews = Review::query()
            ->with(['author', 'reviewrateable'])
            ->latest()
            ->paginate(20);

        return view('admin.reviews.index', compact('reviews'));
    }

    public function approve(Review $review)
    {
        $review->updatedApproved(true);

        return back()->with('success', 'Review approved.');
    }

    public function reject(Review $review)
    {
        $review->delete();

        return back()->with('success', 'Review deleted.');
    }
}

Blade Template Example

{{-- Product rating summary --}}
<div class="rating-summary">
    <div class="average">
        <span class="score">{{ $averageRating ?? 0 }}</span>
        <span class="max">/5</span>
    </div>
    <div class="stars">
        @for ($i = 1; $i <= 5; $i++)
            <svg class="{{ $i <= round($averageRating) ? 'text-yellow-400' : 'text-gray-300' }}">
                <!-- Star icon -->
            </svg>
        @endfor
    </div>
    <p>Based on {{ $reviewCount }} reviews</p>
</div>

{{-- Rating distribution --}}
<div class="distribution">
    @foreach ($distribution as $rating => $data)
        <div class="row">
            <span>{{ $rating }} stars</span>
            <div class="bar" style="width: {{ $data['percentage'] }}%"></div>
            <span>{{ $data['count'] }}</span>
        </div>
    @endforeach
</div>

{{-- Reviews list --}}
@foreach ($reviews as $review)
    <div class="review">
        <div class="header">
            <span class="author">{{ $review->author->full_name }}</span>
            <span class="rating">{{ $review->rating }}/5</span>
            <span class="date">{{ $review->created_at->diffForHumans() }}</span>
        </div>
        @if ($review->title)
            <h4>{{ $review->title }}</h4>
        @endif
        <p>{{ $review->content }}</p>
        @if ($review->is_recommended)
            <span class="recommended">✓ Recommends this product</span>
        @endif
    </div>
@endforeach

Use Cases

MethodUse Case
averageRating()Display product rating stars
countRating()Show total review count
ratingPercent()Build rating distribution chart
getApprovedRatings()Display reviews on product page
getNotApprovedRatings()Admin review moderation queue
getRecentUserRatings()Customer review history