Загрузка файлов в Laravel: пошаговая инструкция
За полчаса вы сделаете форму, которая принимает файл, проверяет его и сохраняет так, чтобы потом его можно было отдать пользователю. Laravel 11.x, локальное хранилище, без лишней теории.

В конце у вас будет работающая форма загрузки: пользователь выбирает файл, Laravel проверяет тип и размер, сохраняет в storage и показывает ссылку на скачивание. На чистом проекте это занимает около 30 минут. Статья написана для Laravel 11.x; в 10.x всё то же самое, отличия отмечу отдельно.
Что нужно до начала
Проверьте, что окружение готово. Иначе споткнётесь на середине.
- PHP 8.2 или новее (требование Laravel 11);
- установленный Laravel-проект и запущенный сервер разработки:
php artisan serve; - расширение PHP
fileinfo— оно включено по умолчанию в большинстве сборок и нужно для проверки MIME-типа; - права на запись в каталог
storage/app— на локальной машине обычно есть, на сервере проверьте отдельно.
Ещё один момент — лимиты самого PHP. Даже идеальный код не примет файл больше, чем позволяют upload_max_filesize и post_max_size в php.ini. По умолчанию это часто 2M. Если планируете принимать файлы крупнее, поднимите оба значения и перезапустите PHP-FPM или сервер.
Шаг 1. Настройте публичный диск
Laravel хранит загруженное в storage/app. Чтобы файлы были доступны по ссылке, используют диск public и симлинк из public/storage. Создайте его командой:
php artisan storage:link
Проверка: в каталоге public/ появится символическая ссылка storage, ведущая на storage/app/public. Если команда написала «The [public/storage] link has been connected» — всё готово.
Шаг 2. Форма загрузки
Создайте вид resources/views/upload.blade.php. Ключевые детали: атрибут enctype="multipart/form-data" у формы (без него файл не долетит) и директива @csrf (без неё Laravel отклонит запрос с ошибкой 419).
<!-- resources/views/upload.blade.php -->
<form action="{{ route('files.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="document">
<button type="submit">Загрузить</button>
</form>
@if (session('uploaded_url'))
<p>Файл загружен: <a href="{{ session('uploaded_url') }}">скачать</a></p>
@endif
@error('document')
<p style="color:red">{{ $message }}</p>
@enderror
Проверка: позже, открыв страницу в браузере, вы увидите поле выбора файла и кнопку.
Шаг 3. Маршруты
В routes/web.php добавьте два маршрута — показать форму и принять файл.
// routes/web.php
use App\Http\Controllers\FileUploadController;
Route::get('/upload', [FileUploadController::class, 'show']);
Route::post('/upload', [FileUploadController::class, 'store'])->name('files.store');
Шаг 4. Контроллер с проверкой и сохранением
Сгенерируйте контроллер:
php artisan make:controller FileUploadController
Теперь код. Метод validate проверяет, что файл пришёл, это действительно файл, разрешённого типа и не больше лимита. Размер в правиле max задаётся в килобайтах, поэтому 5120 — это 5 МБ.
<?php
// app/Http/Controllers/FileUploadController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class FileUploadController extends Controller
{
public function show()
{
return view('upload');
}
public function store(Request $request)
{
// Правила: файл обязателен, только PDF/JPG/PNG, максимум 5 МБ
$request->validate([
'document' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:5120'],
]);
// store() кладёт файл в storage/app/public/documents
// и возвращает относительный путь со случайным именем
$path = $request->file('document')->store('documents', 'public');
// Публичная ссылка на файл
$url = Storage::disk('public')->url($path);
return back()->with('uploaded_url', $url);
}
}
Проверка: откройте /upload, выберите PDF или картинку до 5 МБ, отправьте. Под формой появится ссылка «скачать». Файл при этом ляжет в storage/app/public/documents под случайным именем вида a1b2c3....pdf.
Почему случайное имя, а не оригинальное
Метод store() сам генерирует уникальное имя. Это защищает от двух проблем сразу: два пользователя не перезапишут файлы друг друга, а имя не станет вектором атаки через путь. Если оригинальное имя нужно показать пользователю — сохраните его отдельным полем в базе, но на диске держите случайное.
Никогда не доверяйте оригинальному имени файла и его расширению. Расширениеmimesпроверяет реальное содержимое черезfileinfo, и именно на него нужно опираться.
Шаг 5. Если нужно сохранить под своим именем
Иногда имя на диске должно быть предсказуемым — например, аватар пользователя user_42.jpg. Тогда используйте storeAs:
$extension = $request->file('document')->extension(); // определяется по содержимому
$name = 'user_' . auth()->id() . '.' . $extension;
$path = $request->file('document')->storeAs('avatars', $name, 'public');
Метод extension() возвращает расширение по реальному MIME-типу, а не по тому, что прислал браузер. Если хотите взять расширение именно из присланного имени — есть clientExtension(), но доверять ему для проверок нельзя.
Соответствие правил валидации
| Правило | Что проверяет | Пример значения |
|---|---|---|
required | Поле присутствует и не пустое | — |
file | Загрузка прошла без ошибок и это файл | — |
mimes | Расширение по реальному содержимому | pdf,jpg,png |
mimetypes | Точный MIME-тип | application/pdf |
max | Размер в килобайтах | 5120 = 5 МБ |
dimensions | Ширина/высота для изображений | max_width=2000 |
Если не получилось
- Ошибка 419 Page Expired. В форме нет
@csrfлибо истёк токен. Добавьте директиву внутрь<form>и перезагрузите страницу. - «The document field is required», хотя файл выбран. У формы нет
enctype="multipart/form-data"или вinputимя не совпадает с ключом вvalidate(name="document"и правило'document' =>). - Файл до лимита PHP, но валидация говорит «too large». Значение в
maxзадаётся в килобайтах.max:5— это 5 КБ, а не 5 МБ. Проверьте число. - Крупный файл вообще не долетает, страница пустая или 413. Упёрлись в
upload_max_filesize/post_max_sizeвphp.iniили в лимит веб-сервера (у Nginx этоclient_max_body_size). Поднимите значения и перезапустите PHP и сервер. - Ссылка на файл открывает 404. Не создан симлинк. Выполните
php artisan storage:linkи проверьте, чтоAPP_URLв.envсоответствует адресу, по которому открыт сайт.
Что дальше
Форма работает — дальше разумно вынести правила в отдельный класс FormRequest через php artisan make:request, чтобы контроллер не разрастался. Если файлов будет много, посмотрите в сторону хранилища S3: код почти не меняется, вы просто указываете диск s3 вместо public — настройка драйверов описана в разделе Filesystem официальной документации. Обработка изображений (сжатие, превью) — отдельная тема, здесь мы её не трогаем; для неё обычно подключают пакет типа Intervention Image.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: Laravel Documentation — File Storage, Laravel Documentation — Validation (File rules)
Частые вопросы
У меня Laravel 10, а не 11. Всё это сработает?
Как принимать несколько файлов сразу?
Файл загружается, но по ссылке 404. Что не так?
Насколько правило mimes защищает от загрузки вредоносного файла?
Где хранятся файлы физически и попадут ли они в git?
Как ограничить размер картинки по ширине и высоте, а не только по весу?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.