Работа с файлами в Laravel

Август, 2022

работа с дисками

Для работы с файлами 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];
highlight by torchlight.dev

posts - имя диска по которому мы будем обращаться.
root - путь к корневой директории, в storage_path() указывается путь относительно storage/
url - адрес для генерации ссылки на файл.

Создаём симлинк public/storage

1php artisan storage:link
highlight by torchlight.dev

Создаём директорию командой или любым удобным способом и, если необходимо, выставляем права:

1mkdir -p storage/app/public/posts
highlight by torchlight.dev

Пишем простую форму создания поста:

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>
highlight by torchlight.dev

Обрати внимание на @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}
highlight by torchlight.dev

Здесь есть некоторые проблемы, но для примера этого достаточно, ибо нас интересует только 14 строка. Разберём её подробнее.

Согласно документации это один из простых способов быстро сохранить файл из запроса.

Первым аргументом в store() указывается относительный путь от диска, позволяющий дополнительно указать директорию в которую сохранить файл. Это может быть удобно для разделения файлов внутри диска, например, мы можем сохранить оригинальное изображение в папку original, а миниатюру в папку preview. В данном примере сохраняем в корень диска, поэтому значение оставляем пустым.

1// original/xxx.jpg
2$request->file('cover')->store('original', 'posts');
3 
4// preview/xxx.jpg
5$request->file('cover')->store('preview', 'posts');
6 
7// xxx.jpg
8$request->file('cover')->store('', 'posts');
highlight by torchlight.dev

Вторым аргументом указывается имя диска. Метод возвращает относительный путь к файлу вместе с именем и расширением, его и сохраняем в БД.

Обрати внимание, что 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="" />
highlight by torchlight.dev

Если это выглядит громоздко, то можно сделать в модели аксессор и в шаблоне получится более элегантно:

1<img src="{{ $post->cover_url }}" alt="" />
highlight by torchlight.dev

# Популярные вопросы

Необходимо каждый раз хардкодить имя диска? Нет, если используется только public, то его можно сделать дефолтным. Или можно в модели определить константу и обращаться к ней:

1$request->file('cover')->store('', Post::STORAGE_DISK);
2 
3Storage::disk($post::STORAGE_DISK)->url($post->cover);
highlight by torchlight.dev

Как хранить разные версии изображений? Достаточно сложный вопрос зависящий от ТЗ и тонкостей использования ссылок. В примере выше в методе store() первым аргументом указывается директория. Таким образом можно разделить на original, preview, full_hd, hd, medium, small, 512px, 480x320 и т.д. Хранить можно в разных полях или в массиве/json, всё зависит от того как это будет использоваться и сколько вариантов может быть. Если для фронта потребуются новые форматы, то хранить их в отдельных колонках может стать затруднительно, поскольку придётся каждый раз править БД. В таком случае можно хранить только имя файла, которое во всех директориях будет одинаковое, а директорию подставлять динамически, когда необходимо. В некоторых системах размеры могут генерироваться "на лету", но это выходит за рамки данного материала.