Хранилище файлов (диски)
Как сохранять, читать и удалять файлы, не привязываясь к папке upload
Аватары, вложения форума, обложки статей, скриншоты — всё это файлы, которые надо куда-то положить и потом отдать посетителю. Раньше каждый модуль делал это сам: UPLOAD_PATH . 'users/album/' . $userId . '/', mkdir(), unlink(). Теперь между кодом и файловой системой стоит диск — Johncms\Storage\StorageInterface.
Диск — это место, где лежат файлы: папка на сервере или объектное хранилище S3. Код работает с диском одинаково в обоих случаях, поэтому переезд файлов в облако — это правка конфигурации, а не модулей.
Главное правило
Обращайтесь к Johncms\Storage\StorageInterface, а не к библиотеке напрямую.
За интерфейсом стоит Flysystem, но её имя не должно встречаться нигде, кроме единственной реализации FlysystemStorage. Так обновление библиотеки — правка одного файла, а не всех мест, где сохраняется файл.
Как пользоваться
Внедрите интерфейс через конструктор — так вы получите диск, который указан в конфигурации как основной:
<?php
declare(strict_types=1);
namespace Johncms\Modules\MyModule\Application\UseCases;
use Johncms\Http\UploadedFileDTO;
use Johncms\Storage\StorageException;
use Johncms\Storage\StorageInterface;
use Johncms\Modules\MyModule\Application\Exceptions\DocumentUploadException;
final readonly class UploadDocumentUseCase
{
public function __construct(
private StorageInterface $storage,
) {
}
public function execute(int $id, UploadedFileDTO $file): void
{
try {
$this->storage->storeFile('documents/' . $id . '.pdf', $file->tmpPath);
} catch (StorageException $exception) {
throw new DocumentUploadException($exception->getMessage());
}
}
}Пути всегда относительные и через прямой слеш. Они считаются от корня диска: у стандартного диска корень — папка public/upload, поэтому documents/5.pdf окажется в public/upload/documents/5.pdf. Никаких UPLOAD_PATH и DS в путях быть не должно.
Папки создаются сами. Диск создаёт всё, чего не хватает в пути, и сразу выставляет права из конфигурации.
Операции
store($path, $contents)
Записывает строку в файл, заменяя то, что там было
storeStream($path, $resource)
То же, но из открытого потока — файл не нужно держать в памяти целиком
storeFile($path, $localFile)
Копирует на диск файл, который уже лежит на сервере (загрузка, импорт)
storeGenerated($path, $handler)
Даёт обработчику путь для записи и кладёт результат на диск
read($path)
Содержимое файла строкой
readStream($path)
Содержимое потоком — для отдачи файла посетителю
exists($path)
Есть ли файл
delete($path)
Удаляет файл. Файла нет — не ошибка
deleteDirectory($path)
Удаляет папку со всем содержимым
copy($from, $to)
Копирует файл внутри диска
size($path)
Размер в байтах
mimeType($path)
MIME-тип
lastModified($path)
Время последней записи, Unix-время
url($path)
Адрес, по которому файл отдаётся, или пустая строка
withLocalCopy($path, $handler)
Даёт обработчику файл на локальной файловой системе
Всё, что диск не смог сделать, приходит как Johncms\Storage\StorageException — ловите её, а не исключения Flysystem.
Запись картинок
Обработчик изображений пишет по пути, а не в память, поэтому для картинок есть storeGenerated(): он даёт временный путь, а результат кладёт на диск сам.
Временный файл удаляется в любом случае — и когда обработчик отработал, и когда он бросил исключение. Расширение временного файла совпадает с расширением цели, потому что формат картинки берётся из расширения.
Чтение туда, где нужен реальный путь
Обработчик изображений, FileInfo, getID3 умеют работать только с путём к файлу. Если диск не на этом сервере, такого пути нет — его даёт withLocalCopy():
На локальном диске обработчик получит сам файл, ничего не копируется. На удалённом файл скачается во временный и удалится после — независимо от того, чем закончился обработчик.
Настройка дисков
Диски описаны в config/autoload/filesystem.global.php, а переопределить их можно в filesystem.local.php — этот файл не входит в поставку и переживает обновление CMS.
driver
На чём построен диск: local или s3
root
Корневая папка диска (для local)
url
Базовый адрес, по которому веб-сервер отдаёт файлы диска. Пустой — диск непубличный
visibility
public или private — какие права получают записанные файлы
permissions
Каким режимам соответствует видимость
options
Настройки драйвера: доступы, регион, бакет
Неизвестное имя драйвера — ошибка при чтении конфигурации, а не «сделаем вид, что это папка на сервере».
Права доступа
Видимость — свойство диска, а не отдельного файла, и она применяется при каждой записи. Это не формальность: локальный драйвер иначе оставляет режим файла на усмотрение umask, и на сервере со строгим umask (например, 0077) загруженный файл получается с правами 0600 — веб-сервер такой файл отдать не сможет. Папки, созданные записью, тоже получают права из конфигурации.
Драйвер S3
Для объектных хранилищ, совместимых с S3 (сам AWS, Selectel, Yandex Object Storage, MinIO и прочие), есть драйвер s3. Он требует пакет, который CMS не поставляет — за ним тянется AWS SDK, а сайту, который хранит файлы у себя, он не нужен:
Если драйвер настроен, а пакет не установлен, диск не соберётся и в сообщении будет команда установки.
Публичные и приватные диски
Диск с непустым url — публичный: файлы отдаёт веб-сервер напрямую, а url() возвращает адрес под этим префиксом.
Диск с пустым url — приватный. Его корень имеет смысл вынести за пределы public/, например в data/. Такие файлы веб-сервер не видит: их отдаёт контроллер CMS по адресу /file/{id}, и url() возвращает пустую строку.
Что это даёт по сравнению с публичной папкой: путь к файлу нельзя подобрать или получить перебором каталога, а удаление записи сразу делает файл недоступным. Проверки «а можно ли этому посетителю этот файл» на уровне ядра пока нет — вложения в разных модулях связаны со своими записями по-разному, и спрашивать пока некого.
Несколько дисков
Если диск известен заранее, внедряйте StorageInterface — контейнер отдаст диск по умолчанию.
Если имя диска становится известно только во время работы (например, оно записано в базе рядом с файлом), возьмите Johncms\Storage\StorageRegistryInterface:
Не запрашивайте у реестра диск с заранее известным именем. Так зависимость перестаёт быть видна в конструкторе, а реестр превращается в service locator.
Где лежат файлы самой CMS
Каждая область файлов знает свои пути в одном месте — так путь не размазывается по десятку use case'ов:
Johncms\Users\UserImages
аватар и фото профиля
Johncms\Modules\Album\Infrastructure\Storage\AlbumPhotoStorage
снимки в альбомах
Johncms\Modules\Forum\Infrastructure\Storage\ForumAttachmentStorage
вложения сообщений форума
Johncms\Modules\Library\Infrastructure\Storage\LibraryCoverStorage
обложки статей библиотеки
Johncms\Modules\Mail\Application\Services\MailFileService
вложения личных сообщений
Johncms\Files\FileStore
всё, что зарегистрировано в таблице files
Заводите такой же класс, когда в модуле появляется своя область файлов. Внутри — диск и методы, названные по смыслу (store, delete, url), снаружи — идентификаторы, а не пути.
Последнее обновление
Это было полезно?