Laravel Image Responses: як віддавати зображення зі зміненим розміром безпосередньо через Routes

Перекладено ШІ 0 Laravel News 15 серпня, 2026

Laravel 13.25 суттєво спрощує роботу із зображеннями, дозволяючи повертати їх безпосередньо з контролерів без зайвого шаблонного коду. Нові методи `fromStream()` та `toFormat()` перетворюють створення динамічних медіаендпоінтів на швидкий та елегантний процес.

Вбудований 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().

# Що ще почитати

Популярні

Інше, що варто прочитати

11 Оновлено 26 червня, 2026

Laravel: шлях до створення справді дієздатних AI-агентів

Чи готові ви підвищити ефективність своїх проектів на Laravel і спростити інтеграцію штучного інтелекту? У нашій статті ви дізнаєтеся, як Vizra ADK може революціонізувати ваш підхід до розробки, розширюючи можливості, забезпечуючи тестування та надійність для ваших AI-агентів

15 Оновлено 26 червня, 2026

Локальні моделі та їх скоупи в Laravel за допомогою атрибута Scope

В Laravel 12 ми отримали можливість використовувати новий підхід для визначення локальних скоупів у моделях Eloquent. Дізнайтеся, як новий атрибут #[Scope] спрощує цей процес і зберігає ваші назви методів незмінними

19 Оновлено 26 червня, 2026

Оптимізація запитів до бази даних за допомогою скорочених методів Laravel

Laravel пропонує зручні методи для роботи з датами, які значно спрощують запити до бази даних. Досліджуйте, як ці інтуїтивно зрозумілі функції допомагають створювати чіткі та зрозумілі умови для роботи з часовими даними!