Storage Integration
In most real applications, images do not live on random URLs — they live on Storage disks. The package integrates with Laravel's filesystem layer, so you can build imgproxy sources from any disk without thinking about whether the disk is public or private.
How It Works
- Public disks yield the disk's
url()— a plain, permanent URL. - Private disks yield a pre-signed
temporaryUrl()— a time-limited URL that imgproxy can use to fetch the file.
A disk is treated as private when it implements providesTemporaryUrls() and its config does not explicitly set 'visibility' => 'public'. Disks that omit the visibility key (common for S3-style drivers) produce pre-signed URLs.
The imgproxy() Macro
The imgproxy() macro is available on any Storage::disk() call and returns a builder:
use Illuminate\Support\Facades\Storage;
// Public disk -> url()
Storage::disk('public')->imgproxy('images/photo.jpg')
->width(800)
->format(Format::Webp)
->url();Private Disks and Pre-signed URLs
Private disks generate a pre-signed temporaryUrl() with a 5-minute default expiration:
// Private disk (S3) -> pre-signed temporaryUrl(), 5 minutes by default
Storage::disk('s3')->imgproxy('products/image.jpg', 3600)
->resize(ResizeType::Fill, 800, 600)
->url();Pass an expiration in seconds as the second argument. The URL is valid for that duration, and imgproxy fetches the source within the window.
The Builder::disk() Method
The builder exposes an equivalent ->disk() method when you need to set the disk inline without the macro:
imgproxy()->image('unused')->disk('s3', 'products/image.jpg', 3600)->width(800)->url();The third argument accepts:
int— seconds from now (e.g.3600for one hour).DateTimeInterface— an absolute expiration time.
use Carbon\Carbon;
// Absolute expiration
imgproxy()->image('unused')
->disk('s3', 'products/image.jpg', Carbon::now()->addHour())
->width(800)
->url();The Visibility Rule
The detection logic checks two conditions:
- The disk's
providesTemporaryUrls()returnstrue(e.g. S3, GCS). - The disk config does not have
'visibility' => 'public'.
If both are true, the disk is private and yields pre-signed URLs. If either condition fails, the disk is public and yields plain url().
This means S3 disks without an explicit 'visibility' => 'public' in config/filesystems.php produce pre-signed URLs — even if the bucket is publicly accessible. Add 'visibility' => 'public' to the disk config to force plain URLs:
// config/filesystems.php
's3' => [
'driver' => 's3',
'visibility' => 'public', // Forces url() instead of temporaryUrl()
// ...
],Examples
Public disk with format conversion
use Illuminate\Support\Facades\Storage;
use Imsus\LaravelImgproxy\Enums\Format;
$url = Storage::disk('public')->imgproxy('images/photo.jpg')
->width(800)
->format(Format::Webp)
->url();Private S3 disk with custom expiration
$url = Storage::disk('s3')->imgproxy('products/image.jpg', 3600)
->resize(ResizeType::Fill, 800, 600)
->url();Builder disk method with absolute expiration
use Carbon\Carbon;
$url = imgproxy()->image('unused')
->disk('s3', 'products/image.jpg', Carbon::now()->addHours(2))
->width(800)
->url();Materializing Processed Images New in v2.1.0
toStorage() is the terminal counterpart of url(): instead of returning a URL, it fetches the processed image from imgproxy and writes it to a destination disk, returning a StoredImage representation of the stored file.
use Imsus\LaravelImgproxy\Enums\Format;
$image = imgproxy()->image('https://example.com/photo.jpg')
->width(800)
->format(Format::Webp)
->toStorage('s3', 'processed/photo.webp');The imgproxy URL is built from the current source and processing options; the response body is streamed to the disk, so large images never load fully into memory. An existing file at the destination path is overwritten. Extra write options (visibility, Content-Type, metadata) pass through to the disk write:
$image = imgproxy()->image('https://example.com/photo.jpg')
->toStorage('s3', 'processed/photo.webp', ['visibility' => 'public']);The returned StoredImage carries the disk and path, and produces URLs with the same public/private rule as sources:
$image->disk(); // 's3'
$image->path(); // 'processed/photo.webp'
$image->name(); // 'photo.webp'
// Public destination disk -> plain object URL
$image->url();
// Private destination disk -> pre-signed temporaryUrl(), 5 minutes by default
$image->url(3600);
// The destination disk adapter, for advanced operations
$image->adapter()->delete($image->path());(string) $image is the same URL, so stored images work directly in Blade: .
Failure behavior
- When imgproxy responds with a non-success status (anything outside 2xx, including redirects), an
ImgproxyStorageExceptionis thrown before any disk write, with the HTTP status and a snippet of the response body in the message. - When the disk write fails, the original error is wrapped in the same exception type. This covers both throwing disks and disks configured with
'throw' => false, which report failures by returningfalseinstead. - The destination disk must exist; a missing disk throws Laravel's usual
InvalidArgumentExceptionbefore any HTTP request is made. - The fetch uses a 30-second timeout that covers the whole request, including the streamed body transfer; for very large renders or slow links, raise it via
Http::timeout()configuration on the request or the HTTP client defaults.
Use Cases
Converting images between storages
The storage-to-storage pipeline: the raw image stays on its origin disk, imgproxy converts it, and the result is written to a different disk:
TIP
imgproxy is a conversion tool that serves the processed bytes — it has no feature to upload results to your storage. toStorage() is the bridge: it fetches the converted output and persists it, so imgproxy acts as a pure conversion service in the middle of your pipeline.
use Imsus\LaravelImgproxy\Enums\Format;
use Imsus\LaravelImgproxy\Enums\ResizeType;
$image = Storage::disk('origin')->imgproxy('raw/photo.jpg')
->resize(ResizeType::Fill, 800, 600)
->format(Format::Webp)
->toStorage('processed', 'converted/photo.webp');
$image->url(); // serve from the processed diskMaterializing variants at upload time
Generate format and size variants once, when the original is uploaded, instead of converting on every request. The stored images are durable — they survive imgproxy instance changes and key rotation, and imgproxy never sits in the request path afterward:
use Imsus\LaravelImgproxy\Enums\Format;
foreach ([300, 600, 1200] as $width) {
Storage::disk('origin')->imgproxy('raw/photo.jpg')
->width($width)
->format(Format::Webp)
->toStorage('cdn', "variants/photo-{$width}.webp", ['visibility' => 'public']);
}Batch conversion with queued jobs
toStorage() is synchronous — one conversion per call, blocking until the bytes are written. For large batches, run the conversions in queued jobs:
WARNING
Do not build the source URL before queueing a job. A private origin disk yields a pre-signed source URL that expires (5 minutes by default); a job that runs later fetches an expired source and fails. Resolve the disk inside the job instead, so the pre-signed URL is fresh at run time.
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Storage;
use Imsus\LaravelImgproxy\Enums\Format;
class ConvertImageJob implements ShouldQueue
{
public function __construct(private readonly string $path) {}
public function handle(): void
{
Storage::disk('origin')->imgproxy($this->path)
->format(Format::Webp)
->toStorage('processed', 'converted/'.$this->path.'.webp');
}
}