Файловое хранилище¶
Namespace Keel\Storage. Небольшой интерфейс Storage над каталогом на диске (LocalStorage)
или чем угодно, до чего дотягивается league/flysystem (FlysystemStorage: S3, SFTP, Google
Cloud, память), реестр Disks с именованными дисками и #[FromFile] для привязки загрузок к
параметрам хэндлера с правилом #[File] на размер, тип и расширение.
// config/storage.php
return static fn(string $projectDir): StorageConfig => new StorageConfig(
disks: [
'local' => DiskConfig::local($projectDir . '/var/storage'),
'public' => DiskConfig::local($projectDir . '/public/uploads', url: '/uploads'),
's3' => DiskConfig::flysystem(S3Filesystem::class, url: 'https://cdn.example.com'),
],
default: 'local',
);
// config/services.php
return new ServicesConfig([StorageProvider::class]);
final class AvatarController
{
public function __construct(private readonly Disks $disks) {}
#[Post('/me/avatar')]
#[Auth]
public function upload(
Principal $user,
#[FromFile, File(maxSize: '2M', mimeTypes: ['image/png', 'image/jpeg'])] UploadedFileInterface $avatar,
): array {
$path = sprintf('avatars/%s.%s', $user->id(), pathinfo((string) $avatar->getClientFilename(), PATHINFO_EXTENSION));
$this->disks->disk('public')->upload($avatar, $path);
return ['url' => $this->disks->disk('public')->url($path)];
}
}
Storage¶
| Метод | Что делает |
|---|---|
write($path, $contents) |
строка или читаемый поток; LocalStorage пишет атомарно (временный файл, затем rename) |
upload(UploadedFileInterface $file, $path) |
перемещает загрузку (потоком); неудачная загрузка — StorageException |
read($path), readStream($path) |
содержимое или поток, который закрывает вызывающий; отсутствующий файл — StorageException |
exists, delete, move, size, lastModified |
удаление отсутствующего файла — не ошибка |
url($path) |
публичный URL под базовым URL диска; на диске без него — StorageException |
Пути относительные, разделитель /; .., абсолютные пути, буквы дисков и NUL-байты
отклоняются (StorageException), так что путь из данных запроса не выйдет за корень.
Отсутствующие каталоги создаются при записи.
Storage в контейнере — диск по умолчанию; Disks::disk('name') выбирает другой.
Flysystem-диск указывает на id сервиса League\Flysystem\FilesystemOperator, который вы
собираете в провайдере (composer require league/flysystem плюс адаптер); FlysystemStorage
оборачивает его и напрямую. Каждый диск создаётся при первом обращении и живёт вместе с воркером.
Загрузки¶
#[FromFile] UploadedFileInterface $avatar берёт multipart-поле с именем параметра
(#[FromFile('photo')] задаёт другое). Nullable-параметр без поля получает null;
не-nullable отвечает 400 Missing file "avatar". Загрузка, отклонённая PHP (превышен размер,
частичная), — 400 с кодом ошибки. Параметр array получает все файлы поля с несколькими
файлами (photos[]). Правила валидации на параметре выполняются для каждого файла:
#[File(maxSize: '2M', mimeTypes: ['image/png'], extensions: ['png'])]: размер в байтах или
512K/2M/1G; MIME-типы определяются по первым байтам содержимого через finfo, а не по
Content-Type клиента; расширения берутся из клиентского имени файла без учёта регистра.
Клиентское имя годится только для показа и проверки расширения: путь хранения стройте сами.
В документе OpenAPI такая операция описана с телом multipart/form-data: по свойству
string/binary на файл (массив для поля с несколькими файлами), обязательному, когда
обязателен параметр, рядом с полями DTO из #[FromBody].
Input::file('avatar') и Input::files() читают загрузки без атрибута, см.
HTTP и ядро.