Три методи для трьох ситуацій:
1. download - завантажити файл, що вже є на диску:
return response()->download(storage_path('app/reports/2026-q3.pdf'));
return response()->download($path, 'Звіт за III квартал.pdf'); // інша назва для користувача
return response()->download($path)->deleteFileAfterSend(); // тимчасовий файл
Заголовок Content-Disposition: attachment - браузер пропонує зберегти файл.
2. file - показати файл у браузері (зображення, PDF):
return response()->file(storage_path('app/invoices/42.pdf'));
Content-Disposition: inline - PDF відкриється у вбудованому переглядачі.
3. streamDownload - файл, якого немає на диску, генерується на льоту:
return response()->streamDownload(function () {
$out = fopen('php://output', 'w');
fputcsv($out, ['id', 'email']);
User::query()->lazyById(1000)->each(function (User $user) use ($out) {
fputcsv($out, [$user->id, $user->email]);
});
fclose($out);
}, 'users.csv', ['Content-Type' => 'text/csv']);
Дані віддаються частинами - експорт мільйона рядків не займає мільйона рядків пам'яті й не створює тимчасових файлів.
З дисків Storage (зокрема S3):
return Storage::download('reports/2026-q3.pdf', 'report.pdf');
return Storage::disk('s3')->response('avatars/42.jpg'); // показати
Безпека - головне при віддачі файлів:
- шлях ніколи не будується з введення користувача напряму:
download(storage_path('app/' . $request->file))дозволить завантажити../../.env(обхід шляху). Файл шукають за ідентифікатором у базі, де записано шлях; - перевірка прав перед віддачею - політика чи
Gate, як для будь-якого ресурсу; - приватні файли не кладуть у
public/чи публічний диск - вони мають віддаватися лише через контролер з перевіркою; - назва файлу від користувача - через
download($path, $safeName), Laravel сам коректно закодує не-ASCII символи в заголовку.
Для великих файлів на S3 краще не проганяти їх через PHP взагалі: тимчасове підписане посилання Storage::temporaryUrl() - і браузер завантажує прямо зі сховища.