Библиотека для загрузки файлов с валидацией, конвертацией изображений и Fluent Interface.
- PHP 8.2+
- ext-fileinfo
- ext-gd (опционально, для конверсии изображений)
composer require karelwintersky/arris.php-file-uploaduse 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);
}Проверяет 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; // размер файла доступен даже при ошибке
}Если 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,
]);FileUpload::applyOption('targetPath', '/var/www/photos/');
FileUpload::applyOption('targetMimeType', 'image/webp');
FileUpload::applyOption('targetImageQuality', 85);
FileUpload::applyOption('minFileSize', 1024);
FileUpload::applyOption('locale', 'en');$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));Генератор решает, какое имя (без пути) получит сохраняемый файл внутри 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 : '');
}ensureUploadedResult()готовит дескриптор: берёт уже кэшированный результатuploaded(); если вызывался толькоvalidate()— собирает дескриптор из провалидированного файла; иначе — запускаетuploaded().- Генератор вызывается до
move_uploaded_file()и до конверсии. - Если генератор не установлен — дефолт: исходное имя, а при коллизии имени в
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;
});Валидация выполняется в следующем порядке:
- Прекондишины (fail-fast): файл не задан → не загружен через HTTP → PHP upload-ошибка
- Встроенные валидаторы: MIME-тип → минимальный размер → максимальный размер
- Кастомные валидаторы: все по порядку
Встроенные и кастомные валидаторы работают в режиме 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"
}// 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.
Встроенные 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.
}
}Объект возвращаемый 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\*.
Конвертирует изображения между форматами (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.
Обёртка над 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 при ошибке.
Статический хелпер для работы с размерами файлов и лимитами загрузки:
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