---
title: "Review photos — Reviews for Laravel | Roundly"
description: "Attach photo galleries to reviews from uploads, drafts, disks or URLs, and render responsive images via Media Library."
url: https://roundly-consulting.com/open-source/docs/reviews-for-laravel/review-photos
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Reviews for Laravel](https://roundly-consulting.com/open-source/docs/reviews-for-laravel.md)

# 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:

```php
$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, and the files the earlier photos already wrote (originals and variants) are removed.

## 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:

```php
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

```php
$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

```php
$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, whatever photos.enabled says, so photos stored while the feature was on never outlive their review. 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():

```bash
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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/reviews-for-laravel.md)
