Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FileUpload - PHP8 File Upload Library

Библиотека для загрузки файлов с валидацией, конвертацией изображений и Fluent Interface.

Требования

  • PHP 8.2+
  • ext-fileinfo
  • ext-gd (опционально, для конверсии изображений)

Установка

composer require karelwintersky/arris.php-file-upload

Быстрый старт

use Arris\Toolkit\FileUpload;

$upload = FileUpload::fromFile($_FILES['photo'], 0)
    ->setTargetPath('/var/www/uploads/')
    ->allowMimeTypes(['image/jpeg', 'image/png'])
    ->setMaxFileSize(5 * 1024 * 1024);

$result = $upload->process();

if ($result->isSuccess) {
    echo $result->fullPath;
} else {
    echo implode(', ', $result->errors);
}

Два этапа загрузки

1. uploaded() — проверка первичной загрузки

Проверяет is_uploaded_file(), выполняет полную валидацию (MIME, размер, кастомные валидаторы). Возвращает FileUploadResult с stage=uploaded. На этапе uploaded уже доступны size и mimeType.

$upload = FileUpload::fromFile($_FILES['photo'], 0);

$check = $upload->uploaded();

if (!$check->isSuccess) {
    echo $check->errors[0];
    echo $check->size; // размер файла доступен даже при ошибке
}

2. process() — конверсия, сохранение

Если uploaded() уже вызван и прошёл успешно — process() пропускает валидацию (флаг validated). Если вызван напрямую — выполняет валидацию сам.

$result = $upload->process();

if ($result->isSuccess) {
    echo $result->savedName;    // "2024_01_15__a1b2c3d4.jpg"
    echo $result->fullPath;     // "/var/www/uploads/2024_01_15__a1b2c3d4.jpg"
    echo $result->radix;        // "2024_01_15__a1b2c3d4"
    echo $result->mimeType;     // "image/jpeg"
    echo $result->size;         // 102400
    echo $result->width;        // 1920 (для изображений)
    echo $result->height;       // 1080 (для изображений)
}

Конфигурация

Дефолтный конфиг (один раз при бутстрапе)

FileUpload::setDefaultConfig([
    'targetPath'        => '/var/www/uploads/',
    'allowedMimeTypes'  => ['image/jpeg', 'image/png', 'image/webp'],
    'maxFileSize'       => 10 * 1024 * 1024,
    'minFileSize'       => 1024,
    'filenameGenerator' => function (FileUploadResult $source) {
        $ext = strtolower(pathinfo($source->originalName ?? '', PATHINFO_EXTENSION));
        return date('Y_m_d_') . uniqid(more_entropy: true) . ".{$ext}";
    },
    'throwExceptions'   => false,
]);

Опция через applyOption()

FileUpload::applyOption('targetPath', '/var/www/photos/');
FileUpload::applyOption('targetMimeType', 'image/webp');
FileUpload::applyOption('targetImageQuality', 85);
FileUpload::applyOption('minFileSize', 1024);
FileUpload::applyOption('locale', 'en');

Fluent-конфигурация на инстансе

$upload = FileUpload::fromFile($_FILES['photo'], 0)
    ->setTargetPath('/var/www/uploads/')
    ->allowMimeTypes(['image/jpeg', 'image/png'])
    ->setMaxFileSize(5 * 1024 * 1024)
    ->setMinFileSize(1024)
    ->setTargetMimeType('image/webp', 85)
    ->setLocale('en')
    ->setFilenameGenerator(fn(FileUploadResult $source) => uniqid() . '.' . pathinfo($source->originalName ?? '', PATHINFO_EXTENSION));

Генератор имени файла (filenameGenerator)

Генератор решает, какое имя (без пути) получит сохраняемый файл внутри targetPath. Устанавливается тремя способами: в setDefaultConfig(...), через applyOption('filenameGenerator', ...) или fluent setFilenameGenerator(callable).

Текущая сигнатура (breaking change от 2026-09-18): генератор принимает один аргументFileUploadResult стадии uploaded (дескриптор исходного файла) и возвращает строку — имя файла с расширением:

function (FileUploadResult $source): string

Что доступно внутри генератора

Все метаданные уже вычислены библиотекой на стадии uploaded(), генератору не нужно читать файл повторно:

Поле Что это Типичное применение
$source->mimeType Реальный MIME по содержимому файла (mime_content_type(tmp_name)), а не $_FILES[*]['type'] — тот приходит от браузера и ему доверять нельзя Расширение по содержимому
$source->tmpName Временный путь загруженного файла ($_FILES[*]['tmp_name']) Анализ, копирование
$source->relativePath Путь, как его прислал клиент ($_FILES[*]['full_path']); для одиночной загрузки совпадает с originalName Расширение из клиентского пути, сохранение подкаталогов
$source->originalName Исходное имя файла на клиенте Фолбэк для расширения/радикса
$source->width / $source->height Геометрия изображения (только image/*) Суффикс размера
$source->size Размер в байтах Суффикс размера

Типовой сценарий: расширение по реальному содержимому

Классическая ошибка — брать расширение из originalName: пользователь может загрузить .jpg под именем .png, и файл сохранится с неверным расширением (файл на диске ↔ запись в БД разойдутся). Правильно — определять расширение по реальному MIME с фолбэком на имя, если формат не распознан:

$filenameGenerator = function (FileUploadResult $source) {
    $extension = match ($source->mimeType) {
        'image/jpeg' => 'jpg',
        'image/png'  => 'png',
        'image/webp' => 'webp',
        'image/heic' => 'heic',
        'video/mp4'  => 'mp4',
        'video/webm' => 'webm',
        default      => '',
    };

    // MIME не распознан — берём расширение из клиентского пути/имени
    if ($extension === '') {
        $extension = strtolower((string) pathinfo(
            $source->relativePath ?: $source->originalName ?: '',
            PATHINFO_EXTENSION
        ));
    }

    if ($extension === 'jpeg') {
        $extension = 'jpg';
    }

    return date('Y_m_d_') . uniqid(more_entropy: true) . ($extension ? ".{$extension}" : '');
};

Хорошо показал себя следующий генератор:

$filenameGenerator = function (FileUploadResult $source) {
    // Вычисляем расширение на основе реального MIME-типа
    $extension
        = !is_null($source->mimeType)
        ? MimeTypes::fromType($source->mimeType)
        : strtolower((string)pathinfo($source->relativePath ?: $source->originalName ?: '', PATHINFO_EXTENSION));

    // нормализуем расширение
    if ($extension === 'jpeg') {
        $extension = 'jpg';
    }

    // генерируем вполне вероятно уникальное имя
    $uuid = uniqid(more_entropy: true);
    $dt = date("Y_m_d"); // на базе даты
    
    // возвращаем что-то в духе: 2026_09_18__6aad4aba67f2a6.43778436.jpg
    return $dt . '__' . $uuid . ($extension ? '.' . $extension : '');
}

Порядок вызова внутри process()

  1. ensureUploadedResult() готовит дескриптор: берёт уже кэшированный результат uploaded(); если вызывался только validate() — собирает дескриптор из провалидированного файла; иначе — запускает uploaded().
  2. Генератор вызывается до move_uploaded_file() и до конверсии.
  3. Если генератор не установлен — дефолт: исходное имя, а при коллизии имени в targetPathname_1.ext, name_2.ext, ...

Примечания

  • Генератор возвращает только имя файла, без targetPath.
  • При заданной конверсии (targetMimeType) библиотека сама подменит расширение через changeExtension() (например image/jpeg.jpg) — возвращать итоговое расширение целевого формата в генераторе не требуется.
  • Возвращённое значение не проходит санитизацию: для безопасного имени (без /, .., спецсимволов) нормализуйте его внутри генератора.
  • MIME-детект выполняется один раз на экземпляр и кэшируется (detectMimeType()); повторные mime_content_type() в генераторе избыточны — используйте готовый $source->mimeType.

Валидация

Встроенные валидаторы

$upload = FileUpload::fromFile($_FILES['photo'], 0)
    ->allowMimeTypes(['image/jpeg', 'image/png'])
    ->setMaxFileSize(5 * 1024 * 1024)
    ->setMinFileSize(1024);

Кастомные валидаторы

Функции-коллбэки, которые принимают массив файла и возвращают:

  • true — валидация пройдена
  • false — валидация не пройдена, в ошибки запишется дефолтное сообщение "Ошибка валидации файла"
  • строка — валидация не пройдена, в ошибки запишется указанная строка
$upload = FileUpload::fromFile($_FILES['document'], 0)
    ->setTargetPath('/var/www/docs/')
    ->addValidator(function (array $file): bool|string {
        if ($file['size'] < 1024) {
            return 'Файл слишком маленький (минимум 1KB)';
        }
        return true;
    })
    ->addValidator(function (array $file): bool|string {
        if (preg_match('/[^a-zA-Z0-9._-]/', $file['name'])) {
            return 'Имя файла содержит недопустимые символы';
        }
        return true;
    });

Порядок и collect-all

Валидация выполняется в следующем порядке:

  1. Прекондишины (fail-fast): файл не задан → не загружен через HTTP → PHP upload-ошибка
  2. Встроенные валидаторы: MIME-тип → минимальный размер → максимальный размер
  3. Кастомные валидаторы: все по порядку

Встроенные и кастомные валидаторы работают в режиме collect-all: все проверки выполняются, все ошибки собираются. Пользователь видит все проблемы сразу, а не только первую попавшуюся.

// Пример: файл слишком маленький + неверный MIME — обе ошибки в одном ответе
FileUpload::applyOption('allowedMimeTypes', ['image/png']);
FileUpload::applyOption('minFileSize', 300 * 1024);

$result = $upload->uploaded();
// $result->errors = ['Файл слишком маленький', 'Недопустимый тип файла: image/jpeg']

Конверсия изображений

Конвертирует изображение из одного формата в другой при перемещении в storage.

Для этого нужно указать целевой mime-тип и качество. Третий параметр $force заставляет применить конвертер даже если целевой mime-тип совпадает с исходным — это позволяет приводить загруженные фотографии к общему стандарту (пережатие).

$upload = FileUpload::fromFile($_FILES['photo'], 0)
    ->setTargetPath('/var/www/images/')
    ->allowMimeTypes(['image/jpeg', 'image/png'])
    ->setTargetMimeType('image/webp', 85);

$result = $upload->process();

if ($result->isSuccess) {
    echo $result->extension; // "webp"
}

Принудительная конвертация ($force)

// JPEG → JPEG, но с пережатием до качества 75
$upload->setTargetMimeType('image/jpeg', 75, true);

Кастомный конвертер

FileUpload::applyOption('conversionCallback', function (
    string $sourcePath,
    string $targetPath,
    string $targetMime,
    int $quality
): bool {
    return copy($sourcePath, $targetPath);
});

Поддерживаемые форматы

Формат Источник Цель
JPEG yes yes
PNG yes yes
GIF yes yes
WebP yes yes
BMP yes

Система ошибок

Коды ошибок

Каждая ошибка имеет код FileUploadErrorCode (backed enum). Доступны через getErrorStack():

$stack = $upload->getErrorStack();
// [
//     ['code' => ErrorCode::FILE_TOO_LARGE, 'params' => []],
//     ['code' => ErrorCode::VALIDATOR_FAILED, 'params' => ['message' => 'Файл повреждён']],
// ]

foreach ($stack as $entry) {
    echo $entry['code']->value; // 'file_too_large'
}

Трансляция сообщений

getErrors() возвращает массив человекочитаемых строк (через FileUploadErrorMessages):

$errors = $upload->getErrors();
// ['Файл слишком большой', 'Файл повреждён']

Кастомизация сообщений

use Arris\Toolkit\FileUpload\ErrorCode;
use Arris\Toolkit\FileUpload\ErrorMessages;

// Одно сообщение
ErrorMessages::setMessage(
    ErrorCode::FILE_TOO_LARGE,
    'Максимум 10 МБ!'
);

// Пакетная замена (удобно для i18n)
ErrorMessages::setMessages([
    'file_too_large'  => 'Maximum 10 MB',
    'file_too_small'  => 'Minimum 1 KB',
    'invalid_mime_type' => 'Unsupported file type: {mime_type}',
    'validator_failed'  => '{message}',
]);

Параметры в шаблонах: {mime_type}, {message} — подставляются из params.

Локализация (i18n)

Встроенные locales: ru (по умолчанию) и en.

use Arris\Toolkit\FileUpload;

// На инстансе
$upload = FileUpload::fromFile($_FILES['file'])
    ->setLocale('en');

// Глобально через конфиг
FileUpload::setDefaultConfig([
    'locale' => 'en',
]);

// Или через applyOption
FileUpload::applyOption('locale', 'en');

// Принудительно для текущего результата
$errors = $upload->setLocale('ru')->getErrors();

Обработка ошибок

Тихий режим (по умолчанию)

$result = $upload->process();

if (!$result->isSuccess) {
    echo $result->lastError;
    print_r($result->errors);
}

Режим исключений

use Arris\Toolkit\FileUploadException;

FileUpload::applyOption('throwExceptions', true);

try {
    $result = $upload->process();
} catch (FileUploadException $e) {
    echo $e->getMessage();
    print_r($e->getErrors());
}

Множественная загрузка

$photoKeys = array_keys($_FILES['photos']['tmp_name']);

foreach ($photoKeys as $photoId) {
    $upload = FileUpload::fromFile($_FILES['photos'], $photoId);

    $check = $upload->uploaded();
    if (!$check->isSuccess) {
        continue;
    }

    $result = $upload->process();
    if ($result->isSuccess) {
        // сохраняем $result->radix, $result->mimeType, etc.
    }
}

FileUploadResult

Объект возвращаемый uploaded() и process().

Поле Тип Описание
isSuccess bool Успешность операции
stage string|null 'uploaded' или 'processed'
originalName string|null Оригинальное имя файла
savedName string|null Имя файла в storage
path string|null Путь к каталогу storage
fullPath string|null Полный путь к файлу
mimeType string|null MIME-тип
size int|null Размер в байтах
lastError string|null Последняя ошибка
errors array Массив ошибок
radix string|null Имя файла без расширения
extension string|null Расширение без точки
width int|null Ширина (image/*)
height int|null Высота (image/*)
tmpName string|null Временный путь (tmp_name) исходного файла; заполнен на стадии uploaded
relativePath string|null Путь, как его передал клиент ($_FILES[*]['full_path']); для обычной загрузки = originalName
// Сериализация
$result->toJson();      // JSON строка
$result->toJson(true);  // JSON с форматированием
$result->toArray();     // PHP массив
(string) $result;       // JSON через __toString

Доступные опции

Опция Тип Описание
targetPath string Каталог для сохранения
allowedMimeTypes array Разрешённые MIME-типы
maxFileSize int Максимальный размер (байты)
minFileSize int Минимальный размер (байты)
filenameGenerator callable Генератор имени файла fn(FileUploadResult $source): string
throwExceptions bool Бросать FileUploadException вместо возврата ошибки
validators array Массив callable-валидаторов
targetMimeType string Целевой MIME-тип для конверсии
targetImageQuality int Качество конверсии (0-100)
locale string Локаль для сообщений ошибок ('ru' или 'en')

Вспомогательные классы

Все вспомогательные классы находятся в неймспейсе Arris\Toolkit\FileUpload\*.

ImageConvertor

Конвертирует изображения между форматами (GD). Fluent API:

use Arris\Toolkit\FileUpload\ImageConvertor;

ImageConvertor::from('/path/to/photo.jpg')
    ->toWebP(quality: 85)
    ->save('/path/to/output/');

ImageConvertor::from('/path/to/photo.jpg')
    ->toPng(compression: 9)
    ->save('/path/to/output/');

ImageConvertor::from('/path/to/photo.gif')
    ->toPng(preserveAlpha: true)
    ->save('/path/to/output/');

Поддерживаемые конверсии: JPEG, PNG, GIF, WebP, BMP → JPEG/PNG/GIF/WebP.

MediaProbe

Обёртка над ffprobe для получения метаданных медиафайлов:

use Arris\Toolkit\FileUpload\MediaProbe;

$info = MediaProbe::probe('/path/to/video.mp4');

echo $info->width;     // 1920
echo $info->height;    // 1080
echo $info->codec;     // "h264"
echo $info->duration;  // 125.4
echo $info->isVideo;   // true
echo $info->isAudio;   // false

Возвращает MediaProbeResult (readonly value object) или null при ошибке.

Helper

Статический хелпер для работы с размерами файлов и лимитами загрузки:

use Arris\Toolkit\FileUpload\Helper;

// Конвертация строкового размера в байты
Helper::returnBytes('64M');    // 67108864
Helper::returnBytes('1.5G');   // 1610612736
Helper::returnBytes('1024K');  // 1048576
Helper::returnBytes(1024);     // 1024

// Значение php.ini директивы в байтах
Helper::getIniValue('upload_max_filesize');  // например 20971520 (20M)
Helper::getIniValue('post_max_size');        // например 8388608 (8M)

// Вычисление реального лимита загрузки
$limits = Helper::getUploadLimits('64M');

// $limits['POST_MAX_SIZE']   — post_max_size в байтах
// $limits['UPLOAD_MAX_SIZE'] — upload_max_filesize в байтах
// $limits['CONFIG_MAX_SIZE'] — прикладной лимит (64M) в байтах
// $limits['REAL_MAX_SIZE']   — минимум из трёх
// $limits['IS_WRONG_SIZE']   — true если конфиг превышает физические лимиты

Лицензия

MIT License

About

A library to help with creating uploads for files in PHP

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages