L'API immagine di prima parte di Laravel è stata solida sul fronte scrittura sin dalla 13.20: prendi un upload, lo trasformi, lo metti su un disco. Il percorso di lettura era meno pulito. Servire un'immagine ridimensionata via HTTP significava chiamare toBytes(), costruire una risposta e impostare manualmente il content-type, il che si traduce in tre righe di boilerplate in ogni controller che lo fa.
Laravel 13.25 fa sì che Image implementi il contratto Responsable, quindi un'istanza immagine è un valore di ritorno valido da una route o da un controller. Nella stessa release sono arrivate due aggiunte correlate, Image::fromStream() e un toFormat() pubblico, e insieme queste tre coprono gran parte di ciò che un endpoint di immagini richiede.
Restituire un'immagine
use Illuminate\Support\Facades\Image;
Route::get('/avatars/{user}', function (User $user) {
return Image::fromStorage($user->avatar_path)
->cover(200, 200)
->toWebp()
->quality(80);
});
Questo è tutto qui. Il framework chiama toResponse(), che esegue la pipeline, restituisce i byte elaborati con un 200 e imposta Content-Type dall'output piuttosto che dalla sorgente. La route sopra restituisce image/webp anche se il file salvato è un JPEG, perché l'header viene letto da ciò che la pipeline ha prodotto.
Tutto ciò che restituisce un Image funziona allo stesso modo, quindi un metodo di un controller, un controller invocabile o un valore restituito da una closure di route model binding vanno tutti bene. L'istanza è pigra finché qualcosa non chiede i byte, il che significa che la trasformazione non viene eseguita quando il framework sta solo decidendo che tipo di risposta ha.
Aggiungere header di cache
La risposta predefinita non ha alcun header di caching, che è la scelta giusta per un framework e quella sbagliata per un endpoint che ridimensiona un'immagine a ogni richiesta. Chiama toResponse() tu stesso quando vuoi aggiungervi qualcosa:
Route::get('/avatars/{user}', function (Request $request, User $user) {
return Image::fromStorage($user->avatar_path)
->cover(200, 200)
->toWebp()
->quality(80)
->toResponse($request)
->setMaxAge(31536000)
->setPublic();
});
toResponse() restituisce un Illuminate\Http\Response, quindi è disponibile tutta l'API di risposta: header(), setEtag(), setLastModified() e il resto. Se abbini un max-age lungo a un URL che cambia quando l'immagine cambia, tramite un hash nel percorso o una query string costruita dal updated_at del modello, i browser smettono di fare richieste dopo la prima volta.
Per qualsiasi cosa con traffico reale, ridimensionare a ogni richiesta è comunque un lavoro ripetuto. Il pattern che scala è scrivere il file derivato alla prima richiesta e servirlo dal disco successivamente:
Route::get('/thumbs/{photo}', function (Request $request, Photo $photo) {
$path = "thumbs/{$photo->id}-{$photo->updated_at->timestamp}.webp";
if (! Storage::disk('public')->exists($path)) {
Image::fromStorage($photo->path)
->cover(400, 400)
->toWebp()
->quality(80)
->storeAs('thumbs', basename($path), 'public');
}
return Storage::disk('public')->response($path);
});
Mettere il timestamp nel nome del file significa che una foto aggiornata produce un nuovo percorso, quindi le vecchie miniature cadono in disuso senza una cache da invalidare.
Formati dinamici con toFormat()
toFormat()Un endpoint che accetta un formato dalla richiesta richiedeva un'istruzione match per trasformare la stringa nella chiamata di metodo giusta. toFormat() ora è pubblico e accetta direttamente il formato:
Route::get('/photos/{photo}.{format}', function (Photo $photo, string $format) {
return Image::fromStorage($photo->path)
->scale(width: 1200)
->toFormat($format)
->quality(80);
})->where('format', 'webp|avif|jpg');
I valori accettati sono webp, jpg, jpeg, png, gif, avif, heic, heif e bmp, con heif normalizzato in heic. Qualsiasi altro valore lancia un'ImageException con il formato nel messaggio, che è un 500 piuttosto che un 404, quindi vincola il parametro nella route come sopra o valida il valore prima di passarlo. Lo stesso metodo supporta optimize(), che è la versione da usare quando vuoi anche la qualità in un'unica chiamata.
Questo è ciò che rende breve un endpoint AVIF con fallback. Servi il formato richiesto dalla richiesta e lascia che l'elemento <picture> decida quale URL il browser deve contattare.
Costruire da uno stream
Image::fromStream() crea un'istanza da una risorsa stream, coprendo le fonti che gli altri metodi factory non coprono:
$image = Image::fromStream(Storage::disk('s3')->readStream($path));
La lettura è pigra. fromStream() avvolge la risorsa in una closure e non la tocca finché la pipeline non viene eseguita, quindi creare un'istanza che alla fine non usi non costa nulla. Uno stream che non produce dati lancia un'ImageException con il messaggio "Invalid stream image data." in quel momento piuttosto che alla costruzione.
Oltre a fromPath(), fromStorage(), fromUpload(), fromUrl(), fromBytes() e fromBase64(), la variante stream è quella per qualsiasi cosa per cui hai già un handle: un corpo php://input in un endpoint di upload raw, un file letto da uno zip o uno stream passato da un'altra libreria.
Un endpoint completo
Mettendo insieme le tre cose, un endpoint di immagini che accetta larghezza e formato, legge da S3 e memorizza nella cache per un anno:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage;
Route::get('/media/{media}', function (Request $request, Media $media) {
$validated = $request->validate([
'w' => ['integer', 'between:32,2000'],
'format' => ['in:webp,avif,jpg'],
]);
return Image::fromStream(Storage::disk('s3')->readStream($media->path))
->scale(width: $validated['w'] ?? 800)
->toFormat($validated['format'] ?? 'webp')
->quality(80)
->toResponse($request)
->setMaxAge(31536000)
->setPublic();
})->middleware('signed');
Due dettagli da tenere presente. La larghezza è limitata, perché una dimensione non validata su un endpoint pubblico è un invito a chiedere un ridimensionamento di 20.000 pixel. E la route è firmata, il che impedisce a chiunque di generare varianti arbitrarie a tue spese di storage. Le signed routes di Laravel ti permettono di ottenere tutto questo con una chiamata signedRoute() nella view.
Per approfondire
- A Practical Guide to Laravel's First-Party Image Processing copre l'API di trasformazione e storage in dettaglio
- HEIC Image Uploads in Laravel copre il lato input, incluse le foto da telefono
- Extract an Image's Dominant Color in Laravel si abbina bene a un endpoint di resize per gli sfondi placeholder
- Pause All Queues and a New artisan dev UI in Laravel 13.25 contiene le note di rilascio complete
- Tutte e tre le modifiche sono state apportate da Caleb White in #61111, #61109 e #61110