Search by

karelwintersky / arris.php-file-upload

KarelWintersky

Powerful PHP8 file upload library with validation, conversion, and fluent interface

Package info

github.com/ArrisFramework/Arris.Toolkit.FileUpload

pkg:composer/karelwintersky/arris.php-file-upload

Statistics

Installs: 48

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.4.0 2026-09-18 15:06 UTC

This package is auto-updated.

Last update: 2026-09-18 15:40:00 UTC


README

Библиотека для загрузки файлов с валидацией, конвертацией изображений и 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. Если генератор не установлен — дефолт: исходное имя, а при коллизии имени в targetPath — name_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