Creating reviews
Reviews::for($subject) scopes to the subject and ->by($author) starts a fluent PendingReview; chain setters and finish with create():
use RoundlyConsulting\Reviews\Facades\Reviews;
$review = Reviews::for($restaurant) // the reviewable subject
->by($user) // the author
->rating(5)
->title('Loved it')
->content('Great place')
->meta(['visit' => 'dinner']) // array or Collection
->create();
// Rating-only (no text):
Reviews::for($restaurant)->by($user)->rating(4)->create();
// Text-only (no rating):
Reviews::for($restaurant)->by($user)->content('Friendly staff, slow kitchen')->create();
// Force-approve on create (bypasses moderation):
Reviews::for($restaurant)->by($user)->content('Trusted')->approved()->create();
// Mark a verified purchase / verified reviewer:
Reviews::for($restaurant)->by($user)->rating(5)->verified()->create();Or start from the subject — addReview() goes through the same manager:
// Start from the subject — for($restaurant)->by($user) is filled in for you:
$restaurant->addReview($user)->rating(5)->content('Best ramen in town')->create();Builder reference
| Method | Purpose |
|---|---|
Reviews::for(Model $reviewable) | The subject being reviewed — returns a ReviewableScope. |
->by(Model $author) | The author — returns the PendingReview; subject and author are fixed from here. |
rating(?int $rating) | Score within min_rating…max_rating; null for text-only. |
title(?string $title) | Optional headline. |
content(?string $content) | Optional body text. |
meta(array|Collection|null $meta) | Free-form metadata, stored as JSON. |
approved() | Approve on create, bypassing the moderator. |
verified(bool $verified = true) | Mark a verified purchase / reviewer. |
withPhoto() / withPhotos() | Attach uploaded files (or local paths). |
withDraftPhoto(string $token) | Bind a media-library draft upload. |
withPhotoFromDisk($path, $disk) | Attach a file already on a disk. |
withPhotoFromUrl(string $url) | Download and attach a remote image. |
create() | Validate, moderate, persist and return the Review. |
What create() does
- Rejects an empty review — a review needs a rating, content, or both.
- Validates the rating against min_rating…max_rating.
- With one_per_author on, rejects a second non-deleted review by the same author for the same subject — owner responses don’t count. The check runs again under the author’s row lock, so concurrent submissions can’t both get in.
- Sets the status: approved() or auto_approve approves immediately and stamps approved_at; otherwise the configured moderator decides — with the author and subject already associated — falling back to default_status.
- Saves the review and attaches its photos in one database transaction — a failed upload or an over-limit request rolls the whole create back.
- Dispatches ReviewCreated after the transaction, so listeners see the persisted photos — then ReviewApproved or ReviewRejected when the review lands approved or rejected.
Validation & exceptions
Every package exception extends RoundlyConsulting\Reviews\Exceptions\ReviewException (a RuntimeException), so one catch covers them all:
use RoundlyConsulting\Reviews\Exceptions\InvalidRatingException;
use RoundlyConsulting\Reviews\Exceptions\InvalidReviewException;
use RoundlyConsulting\Reviews\Exceptions\ReviewException;
try {
Reviews::for($restaurant)->by($user)->rating(7)->create();
} catch (InvalidRatingException $e) {
// "The rating 7 must be between 1 and 5."
} catch (InvalidReviewException $e) {
// empty review, duplicate author, too many photos, photos disabled
} catch (ReviewException $e) {
// the common base of every package exception
}| Exception | Thrown when |
|---|---|
InvalidReviewException::empty() | A review has neither a rating nor content. |
InvalidReviewException::duplicate() | one_per_author is on and the author already reviewed the subject. |
InvalidReviewException::tooManyPhotos() | The photos would exceed photos.max. |
InvalidReviewException::photosDisabled() | A photo is attached while photos.enabled is false. |
InvalidRatingException::outOfRange() | A rating falls outside min_rating…max_rating — on create or update. |
The rating, empty, duplicate and banned-word messages are translatable — publish reviews-translations and edit lang/vendor/reviews:
| Key | Default message |
|---|---|
reviews::messages.rating.out_of_range | The rating :given must be between :min and :max. |
reviews::messages.review.empty | A review must have either a rating or content. |
reviews::messages.review.duplicate | This author has already reviewed this subject. |
reviews::messages.review.banned_word | This review contains language that is not allowed. |
The DTO and the action
The subject and author are fixed once by() returns the builder, so it can’t be re-pointed at another subject. create() hands a CreateReviewData to Reviews::create(), which runs the CreateReview action — the same operation in one call:
use RoundlyConsulting\Reviews\DataTransferObjects\CreateReviewData;
use RoundlyConsulting\Reviews\Facades\Reviews;
// The builder's terminal step, in one call:
$review = Reviews::create(new CreateReviewData(author: $user, reviewable: $restaurant, rating: 5));Or run the action yourself (see DI and actions):
use RoundlyConsulting\Reviews\Actions\CreateReview;
use RoundlyConsulting\Reviews\DataTransferObjects\CreateReviewData;
$review = app(CreateReview::class)->execute(new CreateReviewData(
author: $user,
reviewable: $restaurant,
content: 'Great place',
title: 'Loved it',
rating: 5,
meta: collect(['visit' => 'dinner']),
approved: false,
verified: true,
));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.