Вбудований API Laravel для роботи із зображеннями, починаючи з версії 13.20, чудово справлявся із записом: завантаженням, трансформацією та збереженням на диск. Проте зчитування було менш зручним. Щоб віддати змінене зображення через HTTP, доводилося викликати toBytes(), власноруч формувати `response` та вказувати `content type` — це три рядки шаблонного коду в кожному `controller`.
У Laravel 13.25 клас Image реалізує контракт Responsable, тому тепер екземпляр зображення можна повертати безпосередньо з route або controller. Разом із цим з’явилися методи Image::fromStream() та публічний toFormat(). Ця трійка нововведень закриває більшість потреб при створенні endpoint для зображень.
# Повернення зображення
use Illuminate\Support\Facades\Image;
Route::get('/avatars/{user}', function (User $user) {
return Image::fromStorage($user->avatar_path)
->cover(200, 200)
->toWebp()
->quality(80);
});
Це все, що потрібно. Фреймворк сам викликає toResponse(), запускає обробку, повертає готові байти зі статусом 200 та встановлює Content-Type відповідно до вихідного формату, а не початкового файлу. У прикладі вище route поверне image/webp, навіть якщо в сховищі лежить JPEG.
Цей механізм працює скрізь, де можна повернути Image: у методах controller, invokable controller або у замиканнях route model binding. Обробка залишається «лінивою» (lazy) — трансформація не почнеться, поки фреймворку не знадобляться байти для відповіді.
# Додавання кеш-хедерів
Стандартна відповідь не містить хедерів кешування. Це правильний вибір для фреймворку, але невдалий для endpoint, який змінює розмір зображення при кожному запиті. Якщо ви хочете налаштувати кешування, викличте toResponse() самостійно:
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() повертає Illuminate\Http\Response, вам доступний повний API відповідей: header(), setEtag(), setLastModified() тощо. Поєднання тривалого max-age з URL, що змінюється при оновленні картинки (через хеш у шляху або `query string` на основі updated_at), дозволить браузерам не завантажувати файл повторно.
Для високого трафіку зміна розміру «на льоту» — це зайве навантаження. Краще один раз створити похідний файл і надалі віддавати його прямо з диска:
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);
});
Додавання мітки часу у назву файлу гарантує, що при оновленні фото створиться новий шлях, а старі мініатюри просто перестануть використовуватися без потреби в інвалідації кешу.
# Динамічні формати з toFormat()
Раніше для обробки формату із запиту доводилося використовувати оператор match, щоб викликати потрібний метод. Тепер метод toFormat() став публічним і приймає рядок безпосередньо:
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');
Підтримуються формати webp, jpg, jpeg, png, gif, avif, heic, heif та bmp. Будь-які інші значення викличуть ImageException (помилка 500), тому важливо обмежувати параметри route або валідувати вхідні дані. Цей же метод лежить в основі optimize(), який зручно використовувати для одночасного налаштування якості.
# Створення зображення зі Stream
Метод Image::fromStream() створює екземпляр з ресурсу потоку, що дозволяє працювати з джерелами, які не підтримуються іншими фабричними методами:
$image = Image::fromStream(Storage::disk('s3')->readStream($path));
Це «ліниве» зчитування. fromStream() огортає ресурс у замикання і не чіпає його до початку роботи конвеєра обробки. Якщо потік виявиться порожнім, ImageException з повідомленням "Invalid stream image data" виникне лише на етапі виконання, а не при створенні об'єкта.
Разом із fromPath(), fromStorage(), fromUpload(), fromUrl(), fromBytes() та fromBase64(), варіант зі stream ідеально підходить для php://input, читання файлів із ZIP-архівів або потоків з інших бібліотек.
# Повний приклад Endpoint
Ось як виглядає endpoint, що зчитує файл з S3, масштабує його, змінює формат та кешує на рік:
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');
Дві важливі деталі: ми обмежуємо ширину зображення (щоб уникнути запитів на ресайз у 20 000 пікселів) і використовуємо middleware('signed'). Це захищає ваш рахунок за хмарне сховище від генерації довільних варіантів зображень зловмисниками. Створити таку посилання у `view` можна за допомогою signedRoute().
# Що ще почитати
- Практичний посібник із вбудованої обробки зображень у Laravel — детально про трансформації та збереження
- Завантаження HEIC-зображень у Laravel — про роботу з фото зі смартфонів
- Визначення домінантного кольору зображення — корисно для створення плейсхолдерів
- Повний опис релізу Laravel 13.25 — про паузу черг та новий інтерфейс artisan dev
- Ці зміни вніс Caleb White у PR #61111, #61109 та #61110