Для работы с файлами Laravel предоставляет мощную абстракцию файловой системы Storage , реализованную на библиотеке Flysystem
# Быстрый пример
Смотри краткую версию «Как сохранить изображение?»
# Что такое диски
В Storage есть понятие "диски", которые можно свободно настраивать в config/filesystems.php
Каждый диск может хранить любые файлы, быть приватным или публичным, находиться на локальном или удалённом сервере. Зачем это надо? Storage позволяет работать с файлами вне зависимости от их расположения, будь то локальный диск, FTP или Amazon S3. Дисков может быть сколько необходимо, например, с их помощью можно разделить аватарки пользователей, фотографии, медиа-контент в статьях и комментариях, загружаемые пользователями файлы, отчёты и т.д. В дальнейшем можно не беспокоиться, если необходимо будет переместить какие-либо "группы", достаточно просто изменить настройки диска, без изменений в коде и базе данных.
В простых проектах чаще всего используется только два диска local и public и этого может быть вполне достаточно.
# Public
Публичный диск. По умолчанию файлы хранятся в
storage/app/public
Файлы хранящиеся в этом диске будут доступны из web по адресу example.com/storage
Для этого необходимо создать симлинк с помощью команды php artisan storage:link
# Local
Приватный диск. По умолчанию файлы хранятся в
storage/app
Файлы не будут доступны из web, но можно скачать по
временной ссылке
или вернуть их
вручную .
# Default
По умолчанию используется диск local, а значит любые манипуляции с Storage без указания конкретного диска будут происходит с local
Изменить диск по умолчанию можно в .env указав FILESYSTEM_DISK или в config/filesystems.php
Каким делать диск по умолчанию решать тебе, если в проекте используется только public, то удобнее сделать его дефолтным, чтобы не указывать каждый раз. Если в проекте много дисков и их указание неизбежно, то выгоднее оставить local, но это может быть любой диск, зависит от ТЗ.
Напомню, что все настройки дисков можно свободно менять под свои задачи и создавать новые диски, а не ограничиваться предложенными из коробки. В документации отлично описаны возможности системы с примерами.
# Path & URL
Самая популярная проблема у начинающих это связь файлового пути с URL, по этой причине файлы часто сохраняют напрямую в public, но лучше один раз разобраться.
- Файлы хранятся в storage/app/public
- Команда php artisan storage:link создаёт ссылку public/storage
- Путь public/storage ссылается на storage/app/public
- Файл storage/app/public/logo.jpg в web доступен так: https://example.com/storage/logo.jpg
Storage | URI |
---|---|
storage/app/public/logo.jpg | /storage/logo.jpg |
storage/app/public/posts/cover.jpg | /storage/posts/cover.jpg |
storage/app/photo.jpg | недоступен |
# Подробный пример
Рассмотрим простейшую ситуацию с загрузкой изображения к посту.
Начнём с настройки диска. Добавляем новый диск в config/filesystems.php, за основу можно скопировать настройки public.
1return [ 2 // ... 3 'disks' => [ 4 // ... 5 'posts' => [ 6 'driver' => 'local', 7 'root' => storage_path('app/public/posts'), 8 'url' => env('APP_URL').'/storage/posts', 9 'visibility' => 'public',10 'throw' => false,11 ],12 ],13];
posts - имя диска по которому мы будем обращаться.
root - путь к корневой директории, в storage_path() указывается
путь относительно storage/
url - адрес для генерации ссылки на файл.
Создаём симлинк public/storage
1php artisan storage:link
Создаём директорию командой или любым удобным способом и, если необходимо, выставляем права:
1mkdir -p storage/app/public/posts
Пишем простую форму создания поста:
1<form 2 action="{{ route('posts.create') }}" 3 method="POST" 4 enctype="multipart/form-data" 5> 6 @csrf 7 8 <input type="text" name="title" required /> 9 <textarea name="content" required ></textarea>10 11 <input type="file" name="cover" accept="image/png, image/jpeg" />12 13 <button type="submit">Create</button>14</form>
Обрати внимание на @csrf и multipart/form-data , их отсутствие это причина популярных проблем.
Обрабатываем запрос:
1public function store(Request $request) 2{ 3 $data = $request->validate([ 4 'title' => ['required', 'string'], 5 'content' => ['required', 'string'], 6 'cover' => ['nullable', 'image'], 7 ]); 8 9 $post = new Post();10 $post->title = $data['title'];11 $post->content = $data['content'];12 13 if ($request->hasFile('cover')) {14 $post->cover = $request->file('cover')->store('', 'posts');15 }16 17 $post->save();18 19 return redirect()->back()->withSuccess(__('posts.created'));20}
Здесь есть некоторые проблемы, но для примера этого достаточно, ибо нас интересует только 14 строка. Разберём её подробнее.
Согласно документации это один из простых способов быстро сохранить файл из запроса.
Первым аргументом в store() указывается относительный путь от диска, позволяющий дополнительно указать директорию в которую сохранить файл. Это может быть удобно для разделения файлов внутри диска, например, мы можем сохранить оригинальное изображение в папку original, а миниатюру в папку preview. В данном примере сохраняем в корень диска, поэтому значение оставляем пустым.
1// original/xxx.jpg2$request->file('cover')->store('original', 'posts');3 4// preview/xxx.jpg5$request->file('cover')->store('preview', 'posts');6 7// xxx.jpg8$request->file('cover')->store('', 'posts');
Вторым аргументом указывается имя диска. Метод возвращает относительный путь к файлу вместе с именем и расширением, его и сохраняем в БД.
Обрати внимание, что store() генерирует случайное имя файла, а если необходимо задать вручную, то для этого есть метод storeAs()
Файл сохранился здесь: storage/app/public/posts/xxx.jpg
Путь в базе данных: xxx.jpg
URL к файлу: https://example.com/storage/posts/xxx.jpg
Осталось вывести файл в шаблоне. Конечно, можно собрать URL вручную, но у Storage уже есть необходимый метод
1<img src="{{ Storage::disk('posts')->url($post->cover) }}" alt="" />
Если это выглядит громоздко, то можно сделать в модели аксессор и в шаблоне получится более элегантно:
1<img src="{{ $post->cover_url }}" alt="" />
# Популярные вопросы
Необходимо каждый раз хардкодить имя диска? Нет, если используется только public, то его можно сделать дефолтным. Или можно в модели определить константу и обращаться к ней:
1$request->file('cover')->store('', Post::STORAGE_DISK);2 3Storage::disk($post::STORAGE_DISK)->url($post->cover);
Как хранить разные версии изображений? Достаточно сложный вопрос зависящий от ТЗ и тонкостей использования ссылок. В примере выше в методе store() первым аргументом указывается директория. Таким образом можно разделить на original, preview, full_hd, hd, medium, small, 512px, 480x320 и т.д. Хранить можно в разных полях или в массиве/json, всё зависит от того как это будет использоваться и сколько вариантов может быть. Если для фронта потребуются новые форматы, то хранить их в отдельных колонках может стать затруднительно, поскольку придётся каждый раз править БД. В таком случае можно хранить только имя файла, которое во всех директориях будет одинаковое, а директорию подставлять динамически, когда необходимо. В некоторых системах размеры могут генерироваться "на лету", но это выходит за рамки данного материала.