Веб-разработка: обработка пользовательских файлов в PHP

Раздел: Веб-разработка -> Взаимодействие с пользователем

Основные решения для выбора файла в PHP

Как организовать загрузку файла с серверной стороны?

Наиболее эффективный способ - использование формы с атрибутом enctype="multipart/form-data" и последующая обработка суперглобального массива $_FILES. После валидации типа, размера и ошибок файл перемещается в постоянную директорию функцией move_uploaded_file(). Ниже приведён минимальный рабочий пример.


<!-- upload.html -->
<form action="upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="userfile" required>
  <button type="submit">Отправить</button>
</form>

<!-- upload.php -->
<?php
$uploadDir = __DIR__ . '/uploads/';
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
$maxSize = 2 * 1024 * 1024; // 2 MB

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['userfile'])) {
    $file = $_FILES['userfile'];
    
    // Проверка ошибок загрузки
    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Ошибка загрузки: " . $file['error']);
    }
    
    // Проверка размера
    if ($file['size'] > $maxSize) {
        die("Размер файла превышает лимит");
    }
    
    // Проверка MIME-типа через finfo
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, $allowedTypes)) {
        die("Недопустимый тип файла: $mime");
    }
    
    // Генерация безопасного имени
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $newName = md5_file($file['tmp_name']) . '.' . $ext;
    $dest = $uploadDir . $newName;
    
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
    }
    
    if (move_uploaded_file($file['tmp_name'], $dest)) {
        echo "Файл успешно сохранён: " . htmlspecialchars($newName);
    } else {
        echo "Не удалось переместить файл";
    }
}
?>
  

Типичные проблемы и их решения

  • Превышение upload_max_filesize или post_max_size. Проверьте настройки php.ini. Для больших файлов увеличьте лимиты или используйте пошаговую загрузку.
  • Ошибка UPLOAD_ERR_PARTIAL (3). Возникает при обрыве соединения. Рекомендуется увеличить max_input_time.
  • Нет временной директории или прав на запись. Убедитесь, что upload_tmp_dir существует и доступен для записи веб-сервером.
  • MIME-тип не соответствует действительности. Не полагайтесь на $_FILES['type'] - используйте finfo.
  • Проблемы с перемещением файла. Проверьте, что целевая директория существует и имеет права на запись. Избегайте использования оригинального имени - генерируйте уникальное.

Альтернативные варианты

Как загрузить несколько файлов через массив?

Для множественного выбора используется атрибут multiple и имя поля с квадратными скобками: name="files[]". PHP преобразует структуру $_FILES в массив.


<form action="multi.php" method="post" enctype="multipart/form-data">
  <input type="file" name="files[]" multiple>
  <button type="submit">Загрузить</button>
</form>

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['files'])) {
    $files = $_FILES['files'];
    foreach ($files['name'] as $i => $name) {
        if ($files['error'][$i] === UPLOAD_ERR_OK) {
            $tmp = $files['tmp_name'][$i];
            // валидация и перемещение
            move_uploaded_file($tmp, __DIR__ . '/uploads/' . basename($name));
        }
    }
}
?>
  

Проблема: структура $_FILES при multiple неудобна для вложенных массивов. Рекомендуется преобразование через рекурсию.

Как загрузить файл через AJAX с прогрессом?

Клиентская часть использует FormData и XMLHttpRequest (или fetch). Серверный код остаётся тем же, но возвращает JSON.


// frontend.js
const form = document.getElementById('uploadForm');
form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const data = new FormData(form);
    const response = await fetch('upload.php', { method: 'POST', body: data });
    const result = await response.json();
    console.log(result);
});

// upload.php (возвращает JSON)
header('Content-Type: application/json');
$result = ['success' => false];
if ($_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
    // обработка...
    $result['success'] = true;
}
echo json_encode($result);
  

Типичная ошибка: CORS политика при разделении фронтенда и бэкенда. Необходимо настроить заголовки Access-Control-Allow-Origin.

Как принять файл через PUT-запрос?

Для REST API файл может передаваться через PUT. Данные считываются из потока php://input.


<?php
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
    $rawData = file_get_contents('php://input');
    $filename = $_SERVER['HTTP_X_FILENAME'] ?? 'uploaded_file';
    file_put_contents(__DIR__ . '/uploads/' . basename($filename), $rawData);
    echo json_encode(['status' => 'ok']);
}
?>
  

Проблема: потеря метаданных (тип, размер) - их нужно передавать в заголовках. Не подходит для больших файлов без чанков.

Как использовать библиотеки для упрощения?

Фреймворки предлагают готовые компоненты. Например, Symfony HttpFoundation предоставляет класс UploadedFile с удобными методами.


use Symfony\Component\HttpFoundation\File\UploadedFile;

$uploadedFile = new UploadedFile(
    $_FILES['file']['tmp_name'],
    $_FILES['file']['name'],
    $_FILES['file']['type'],
    $_FILES['file']['size']
);

$uploadedFile->move('uploads', $uploadedFile->getClientOriginalName());
  

Примечание: не следует доверять getClientOriginalName() - всегда применяйте санитацию имени.

Расширенные примеры обработки файлов

Пример 1: Надёжная валидация MIME и размера

В этом примере используется finfo для определения реального типа файла, а также проверка на лимит размера и расширения. Код подходит для любых файлов.

Пример

<?php
$allowedMime = ['image/jpeg', 'image/png', 'application/pdf'];
$allowedExt = ['jpg', 'jpeg', 'png', 'pdf'];
$maxSize = 5 * 1024 * 1024; // 5 MB

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
    $f = $_FILES['file'];
    if ($f['error'] !== UPLOAD_ERR_OK) {
        exit("Ошибка " . $f['error']);
    }
    
    // Проверка размера
    if ($f['size'] > $maxSize) {
        exit("Файл слишком большой");
    }
    
    // MIME-тип
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $f['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, $allowedMime)) {
        exit("Недопустимый MIME: $mime");
    }
    
    // Расширение из оригинального имени
    $ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, $allowedExt)) {
        exit("Недопустимое расширение: $ext");
    }
    
    // Успех
    echo "Файл прошёл валидацию. MIME: $mime, размер: " . $f['size'] . " байт";
}
?>
При загрузке файла example.jpg с правильным MIME image/jpeg:
Файл прошёл валидацию. MIME: image/jpeg, размер: 123456 байт

Пример 2: Безопасное сохранение с уникальным именем

Генерация имени на основе хеша содержимого, времени и случайной строки предотвращает конфликты и инъекции.

Пример

<?php
function generateSafeName(string $sourcePath, string $originalName): string {
    $hash = md5_file($sourcePath) . '_' . time() . '_' . random_int(1000, 9999);
    $ext = pathinfo($originalName, PATHINFO_EXTENSION);
    return $hash . '.' . $ext;
}

$destDir = __DIR__ . '/uploads/';
$safeName = generateSafeName($_FILES['file']['tmp_name'], $_FILES['file']['name']);
move_uploaded_file($_FILES['file']['tmp_name'], $destDir . $safeName);
echo "Сохранён как: $safeName";
?>
Сохранён как: a1b2c3d4e5f6_1712345678_7890.png

Пример 3: Автоматический ресайз изображения при загрузке

После загрузки изображения можно уменьшить его до заданных размеров с помощью GD.

Пример

<?php
$maxWidth = 800;
$maxHeight = 600;
$src = $_FILES['image']['tmp_name'];
$info = getimagesize($src);
if (!$info) exit("Не удалось получить размеры");

list($origW, $origH) = $info;
$ratio = min($maxWidth / $origW, $maxHeight / $origH);
$newW = (int)($origW * $ratio);
$newH = (int)($origH * $ratio);

$srcImage = imagecreatefromstring(file_get_contents($src));
$dstImage = imagecreatetruecolor($newW, $newH);
imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $newW, $newH, $origW, $origH);

$dest = __DIR__ . '/uploads/thumb_' . basename($_FILES['image']['name']);
imagejpeg($dstImage, $dest, 85);
imagedestroy($srcImage);
imagedestroy($dstImage);
echo "Создан эскиз: $dest";
?>
Создан эскиз: /var/www/uploads/thumb_photo.jpg
Размеры: 800x600 (при соотношении оригинала 4:3)

Пример 4: Пошаговая загрузка (chunked upload) больших файлов

Разделение файла на части, отправка через AJAX и сборка на сервере. Ниже - упрощённая реализация на PHP.

Пример

<?php
// chunk_upload.php
$chunkDir = __DIR__ . '/chunks/';
$fileName = $_POST['filename'] ?? 'unknown';
$chunkIndex = (int)($_POST['chunk'] ?? 0);
$totalChunks = (int)($_POST['totalChunks'] ?? 1);

$chunkPath = $chunkDir . $fileName . '.part' . $chunkIndex;
move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkPath);

// Если это последний чанк - собираем файл
if ($chunkIndex === $totalChunks - 1) {
    $finalPath = __DIR__ . '/uploads/' . $fileName;
    $out = fopen($finalPath, 'wb');
    for ($i = 0; $i < $totalChunks; $i++) {
        $part = $chunkDir . $fileName . '.part' . $i;
        $in = fopen($part, 'rb');
        stream_copy_to_stream($in, $out);
        fclose($in);
        unlink($part);
    }
    fclose($out);
    echo json_encode(['status' => 'complete', 'path' => $finalPath]);
} else {
    echo json_encode(['status' => 'chunk_received', 'index' => $chunkIndex]);
}
?>
При отправке 3 чанков по 5 МБ:
{"status":"chunk_received","index":0}
{"status":"chunk_received","index":1}
{"status":"complete","path":"/uploads/video.mp4"}

Пример 5: Параллельная обработка нескольких файлов

Использование array_map для одновременного сохранения и валидации массива файлов из формы с multiple.

Пример

<?php
function processFile(array $file): string {
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return "Ошибка: {$file['error']}";
    }
    $dest = __DIR__ . '/uploads/' . basename($file['name']);
    move_uploaded_file($file['tmp_name'], $dest);
    return "OK: {$file['name']}";
}

if (isset($_FILES['files'])) {
    $files = $_FILES['files'];
    $results = array_map(function($i) use ($files) {
        return processFile([
            'name'     => $files['name'][$i],
            'tmp_name' => $files['tmp_name'][$i],
            'error'    => $files['error'][$i],
            'size'     => $files['size'][$i]
        ]);
    }, array_keys($files['name']));
    echo implode("<br>", $results);
}
?>
OK: photo1.jpg
OK: document.pdf
Ошибка: 1 (UPLOAD_ERR_INI_SIZE) для file3.zip

Выбор файла в PHP - comments

En
Php выбрать файл (php)