---
title: "Laravel 13.25: нові можливості роботи з зображеннями через HTTP-маршрути"
url: https://laravelukraine.com/blog/laravel-1325-novi-mozlivosti-roboti-z-zobrazenniami-cerez-http-marsruti
date: 2026-08-15
source: https://laravel-news.com/laravel-image-responses?utm_medium=feed&utm_source=feedpress.me&utm_campaign=Feed%3A+laravelnews
---

# Laravel 13.25: нові можливості роботи з зображеннями через HTTP-маршрути

Вбудований API для роботи із зображеннями у Laravel добре справлявся із записом даних з версії 13.20: прийняти завантажений файл, трансформувати його та зберегти на диск. Проте шлях читання був менш зручним. Щоб віддати змінене зображення через HTTP, доводилося викликати `toBytes()`, створювати відповідь і вручну встановлювати тип контенту - три рядки шаблонного коду у кожному контролері.

## Повернення зображення з маршруту

Laravel 13.25 робить клас `Image` сумісним із контрактом `Responsable`, тому екземпляр зображення можна повертати безпосередньо з маршруту чи контролера. У тому ж релізі з'явилися два пов'язані доповнення: `Image::fromStream()` та публічний метод `toFormat()`. Разом ці три можливості покривають більшість потреб для ендпоінтів із зображеннями.

```php
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` на основі результату, а не вихідного файлу. Наведений маршрут поверне `image/webp`, навіть якщо збережений файл - JPEG, оскільки заголовок читається з того, що створив конвеєр.

Усе, що повертає `Image`, працює однаково: метод контролера, invokable-контролер або значення із замикання route model binding. Екземпляр залишається «лінивим» доти, доки щось не запросить байти, тому трансформація не виконується, коли фреймворк лише визначає тип відповіді.

## Додавання заголовків кешування

Типова відповідь не містить заголовків кешування, що є правильним для фреймворка, але неправильним для ендпоінта, який змінює розмір зображення при кожному запиті. Викличте `toResponse()` самостійно, щоб додати власні заголовки:

```php
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-параметр на основі `updated_at` моделі), і браузери перестануть запитувати після першого разу.

## Масштабування через кешування на диску

Для проєктів із реальним трафіком зміна розміру при кожному запиті - це робота, яку ви виконуєте знову і знову. Підхід, що масштабується: записати похідний файл при першому запиті та віддавати його з диска надалі:

```php
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()` публічний і приймає формат безпосередньо:

```php
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`, де `heif` нормалізується до `heic`. Будь-яке інше значення викине `ImageException` із форматом у повідомленні, що дасть 500, а не 404, тому обмежте параметр у маршруті, як показано вище, або валідуйте значення перед передачею. Цей самий метод лежить в основі `optimize()` - версії для випадків, коли потрібно встановити якість одним викликом.

Це робить AVIF-ендпоінт із резервним форматом коротким. Віддайте той формат, який запросив клієнт, і дозвольте елементу `<picture>` вирішити, який URL завантажить браузер.

## Створення із потоку

Метод `Image::fromStream()` створює екземпляр із потокового ресурсу, що покриває джерела, які не охоплюють інші фабричні методи:

```php
$image = Image::fromStream(Storage::disk('s3')->readStream($path));
```

Читання є лінивим. `fromStream()` обгортає ресурс у замикання та не торкається його до запуску конвеєра, тому створення екземпляра, який ви не використаєте, нічого не коштує. Потік без даних викине `ImageException` із повідомленням "Invalid stream image data." у момент використання, а не при створенні.

Поряд із `fromPath()`, `fromStorage()`, `fromUpload()`, `fromUrl()`, `fromBytes()` та `fromBase64()`, потоковий варіант призначений для будь-чого, дескриптор чого ви вже маєте: тіло `php://input` на ендпоінті прямого завантаження, файл із zip-архіву або потік від іншої бібліотеки.

## Повноцінний приклад ендпоінта

Поєднання всіх трьох можливостей - ендпоінт для зображень, що приймає ширину та формат, читає з S3 і кешує на рік:

```php
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 пікселів. А маршрут підписаний, що запобігає генерації довільних варіантів за ваш рахунок у сховищі. Підписані маршрути Laravel дають це через виклик `signedRoute()` у в'юхі.

## Корисні матеріали

Усі три зміни були внесені Caleb White у pull request'ах #61111, #61109 та #61110. Повні примітки до релізу Laravel 13.25 доступні в офіційному анонсі, де також описано паузу всіх черг та новий UI для artisan dev.
