Skip to main content

Загрузка обязательного файла до создания модели

Многие сущности имеют обязательные поля типа Файл. При этом по дизайн-гайду, передавать в одном запросе поля сущности и файл неправильно. Это неудобно - в случае ошибки валидации пользователю придётся повторить запрос и снова загрузить файл. Такой запрос нужно разбивать на два: один для загрузки файла, второй - для создания сущности. Часто мы не можем загрузить файл после создания сущности - не позволяет дизайн или бизнес-логика. Поэтому файл нужно загрузить в систему до создания сущности, а после, создать сущность, сообщив ей, что она должна использовать ранее загруженный файл.

В Ensi для решения этой проблемы используется т.н. временные файлы. Временный файл - это запись в особой таблице, которая позволяет сослаться на файл для его прикрепления к сущности, или же удалить файл через какое-то время, если он не был ни к чему прикреплён.

Разберём реализацию на примере сервиса PIM.

UploadFileAction

Метод execute принимает следующие агрументы:

  • UploadedFile $file
  • string $folder папка относительно корня публичного диска текущего бизнес-домена
  • string $fileNamePrefix префикс, добавлямый к случайному имени файла

Загруженный файл будет размещён как $folder/hash{0-2}/hash{2-4}/$fileNamePrefix_rand{20}.ext, где:

  • hash{0-2} - это первые два символа хэша от оригинального имения файла
  • hash{2-4} - вторые два символа хэша
  • rand{20} - случайная строка длинной 20 символов
  • ext - оригинальное расширение файла

В ответ вы получите экземпляр класса TempFile.

PreloadFileResource

Классу TempFile соотвествует ресурс PreloadFileResource, который имеет следующую структуру:

return [
'preload_file_id' => $this->id,
'file' => [
'path' => $file->path,
'root_path' => $file->rootPath,
'url' => $file->url,
],
];

Т.е. это id временного файла и рядом обычный ресурс файла, который содержит разные варианты пути до файла.

TempFile

Класс TempFile - это обычная Eloquent модель, у которой есть два вспомогательных метода:

  • $tempFile->evict() - удаляет модель и возвращает путь до файла
  • TempFile::grab($tempFileId) - находит файл по id и вызывает evict() возвращая его результат

Получив запрос на создание сущности, в которой передан preload_file_id вы можете сделать вот так:

$myModel = new MyModel();
// ...
$myModel->file_path = TempFile::grab($preloadFileId);

$myModel->save();

Очистка системы от ненужных временных файлов

В сервисе должна быть реализована консольная команда для периодической очистки хранилища от временных файлов. Принцип работы команды прост - находим все записи в таблице temp_files у которых created_at старше определённого срока хранения файлов, и удаляем файл и запись в таблице.

$timestamp = Date::now()->subHours(self::STORAGE_TIME);
$records = TempFile::where('created_at', '<', $timestamp)->lazyById();

foreach ($records as $record) {
try {
$this->deleteFile($disk, $record);
} catch (Exception $e) {
$this->error("Ошибка при удалении файла {$record->path}: {$e->getMessage()}");
}
}