Как устроены диски:
Диски описаны в config/filesystems.php. Из коробки их три: local для приватных файлов (в Laravel 11+ его корень storage/app/private), public для всего, что можно отдавать вебом (storage/app/public), и s3. Диск по умолчанию задаёт переменная FILESYSTEM_DISK в .env.
Чтобы файлы с диска public открывались по адресу вида /storage/avatars/abc.jpg, нужен симлинк из public/storage на storage/app/public:
Код: Выделить всё
php artisan storage:link
Загрузка из формы:
Форма должна иметь enctype="multipart/form-data", иначе $request->file() вернёт null. Контроллер:
Код: Выделить всё
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\File;
public function store(Request $request)
{
$request->validate([
'avatar' => ['required', File::image()->max('2mb')],
]);
$path = $request->file('avatar')->store('avatars', 'public');
$request->user()->update(['avatar_path' => $path]);
return back()->with('status', 'Аватар обновлён');
}
Приватные файлы:
Счета и договоры не должны лежать в public. Кладём их на local и отдаём через контроллер с проверкой прав, Policies из прошлой главы здесь и пригодятся:
Код: Выделить всё
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
public function download(Invoice $invoice)
{
Gate::authorize('view', $invoice);
return Storage::download($invoice->pdf_path, "invoice-{$invoice->number}.pdf");
}
Подключаем S3:
Код: Выделить всё
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
Код: Выделить всё
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=YCAJE...
AWS_SECRET_ACCESS_KEY=YCM7...
AWS_DEFAULT_REGION=ru-central1
AWS_BUCKET=myapp-prod
AWS_ENDPOINT=https://storage.yandexcloud.net
AWS_USE_PATH_STYLE_ENDPOINT=false
Код: Выделить всё
$url = Storage::disk('s3')->temporaryUrl(
$invoice->pdf_path,
now()->addMinutes(15)
);
Типичные грабли:
Имя файла от клиента. getClientOriginalName() может содержать ../, пробелы, кириллицу и вообще что угодно. В файловую систему оно попадать не должно: хэш-имя от store(), а оригинал, если нужен для отображения, в отдельную колонку БД.
Лимиты PHP и nginx. Правило max('2mb') бессильно, если upload_max_filesize=2M в php.ini обрезал запрос раньше. Проверяйте upload_max_filesize, post_max_size и client_max_body_size у nginx. Симптом: валидация ругается, что файл обязателен, хотя его отправляли, или голая 413.
Забытый storage:link. Локально картинки есть, на проде 404. Особенно коварно при деплое в свежесозданную папку релиза: симлинк остался в старой.
file_get_contents() на гигабайтном экспорте съест память воркера. Для больших файлов есть Storage::putFile(), readStream() и writeStream(), они работают потоками.
exists() на S3 это отдельный HTTP-запрос к хранилищу. Дёргать его в цикле по тысяче записей медленно, а у части провайдеров ещё и платно.
Абсолютные пути в БД. Храните относительный путь внутри диска. Запись вида /var/www/app/storage/... превратит переезд на S3 в ручную миграцию по строкам.
Итог:
Storage даёт единый API над local и S3: публичное раздаём через симлинк, приватное через контроллер или временную ссылку, в БД живут только относительные пути. В следующей главе займёмся тестированием, и там Storage::fake() покажет, зачем была нужна вся эта абстракция: тесты загрузки файлов без единой записи на реальный диск.