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

Review photos

The bundled Review model is a Media Library owner with one photos bucket — a responsive, ordered image gallery. Attach photos straight from the builder:

$review = Reviews::for($restaurant)
    ->by($user)
    ->rating(5)
    ->content('Great place')
    ->withPhoto($request->file('photo'))          // one UploadedFile
    ->withPhotos($request->file('photos'))        // an array of files
    ->withDraftPhoto($token)                       // a media-library draft token
    ->withPhotoFromDisk('incoming/a.jpg', 's3')    // a path on a disk
    ->withPhotoFromUrl('https://example.com/a.jpg')
    ->create();

Photos are bound inside the create transaction, before ReviewCreated fires, so listeners and broadcasts already see them. If any photo fails — or the total exceeds photos.max — the review is not created.

Upload first, review later

Media Library’s draft media lets a form upload photos before the review exists — upload returns a token, and the review binds it on submit:

use RoundlyConsulting\MediaLibrary\Facades\MediaLibrary;

// 1. Upload step — the review does not exist yet. Hand the token back to the client.
$token = MediaLibrary::draft($request->file('photo'))->toBucket('photos')->draft_token;

// 2. Submit step — bind the draft while creating the review.
$review = Reviews::for($restaurant)
    ->by($user)
    ->rating(4)
    ->content('Cosy terrace, friendly staff')
    ->withDraftPhoto($token)
    ->create();

An unknown or already-bound token throws Media Library’s DraftMediaNotFound; an expired one throws DraftMediaExpired.

Reading photos

$review->photos();                          // Collection<Media> (ordered gallery)
$review->hasPhotos();                       // bool
$review->photoCount();                      // int
$review->firstPhotoUrl();                   // string ('' when empty)
$review->firstPhotoUrl('responsive-320');   // a responsive width variant
$review->photoUrls();                       // list<string>
$review->photoUrls('responsive-640');       // every photo at one width
$review->responsivePhotos(['alt' => 'Guest photo', 'class' => 'photo']); // list<string> of <img srcset="…">
$review->resolvePhotoUrl($media);           // one photo's URL
$review->photoSrcset($media);               // one photo's srcset
$review->firstPhotoTemporaryUrl();          // always a signed URL
$review->photosBucket();                    // 'photos' (reviews.photos.bucket)

The bucket generates one responsive variant per width in photos.responsive_widths, named responsive-<width> (responsive-320, responsive-640, responsive-1024 by default). Media Library never upscales, so a width larger than the original is not generated. Until a variant has been generated — or when the photo is narrower than that width — the readers serve the original instead. A name the bucket does not declare (e.g. 'thumb') throws Media Library’s InvalidVariant.

Bucket rules

  • Visibility — public by default; see Private photos for signed-only galleries.
  • Accepted types — photos.accepted_mime_types (JPEG, PNG, WebP and GIF by default).
  • File size — photos.max_file_size, 5 MB by default (0 = no per-photo cap).
  • Count — photos.max per review, 5 by default (0 = unlimited); overflow throws InvalidReviewException::tooManyPhotos().

Photo counts in the summary

$summary = $restaurant->ratingSummary();

$summary->photoCount;          // photos across approved reviews
$summary->reviewsWithPhotos;   // approved reviews carrying at least one photo

Reviews::for($restaurant)->photoCount();
Reviews::for($restaurant)->reviewsWithPhotos();

Lifecycle

  • Warm on approval — when a review becomes approved, a queued ReviewApproved listener dispatches one variant-generation job per photo, so thumbnails are hot the moment the review goes live. That holds however it got there — approve(), an approval on create (approved(), auto_approve or the moderator) or re-moderation after an edit. Turn it off with photos.warm_on_approval.
  • Cleanup — force-deleting a review clears its photos bucket. A soft-deleted (and later restored) review keeps its photos.

Turning photos off

Disable the whole feature and the bucket is never declared, the readers return empty values, and any withPhoto*() call throws InvalidReviewException::photosDisabled():

REVIEWS_PHOTOS_ENABLED=false

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.