PHP-загрузка файлов: примеры кода и рекомендации

Раздел: Веб-разработка -> Загрузка файлов на сервер

Основы загрузки файлов через PHP

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


<!-- form.html -->
<form action="upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="file" />
  <input type="submit" value="Загрузить" />
</form>
  

отправка файла php (отправка файла через php)


<?php
// upload.php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
    $file = $_FILES['file'];
    $uploadDir = __DIR__ . '/uploads/';
    $uploadFile = $uploadDir . basename($file['name']);

    // Проверка ошибок
    if ($file['error'] !== UPLOAD_ERR_OK) {
        // Обработка ошибки
    }

    // Проверка типа (MIME)
    $allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);

    if (!in_array($mimeType, $allowedTypes)) {
        // Недопустимый тип
    }

    // Проверка размера
    $maxSize = 2 * 1024 * 1024; // 2 MB
    if ($file['size'] > $maxSize) {
        // Слишком большой файл
    }

    // Перемещение файла
    if (move_uploaded_file($file['tmp_name'], $uploadFile)) {
        echo "Файл успешно загружен: " . htmlspecialchars($file['name']);
    } else {
        echo "Ошибка при сохранении файла.";
    }
}
?>
  

Цель данного подхода - надёжная загрузка любых файлов с минимальными затратами ресурсов. Используется в большинстве веб-приложений (аватары, документы, изображения).

Типичные ошибки:

  • UPLOAD_ERR_INI_SIZE / UPLOAD_ERR_FORM_SIZE - превышен лимит размера, заданный в php.ini или форме.
  • UPLOAD_ERR_PARTIAL - файл загружен частично (обрыв соединения).
  • UPLOAD_ERR_NO_FILE - файл не выбран.
  • Проблемы с правами на запись в папку uploads.
  • Исполняемые скрипты (PHP, JS) могут быть опасны - требуется дополнительная проверка.

Решение: установить корректные лимиты в php.ini (upload_max_filesize, post_max_size), использовать проверку MIME-типа через finfo (не только расширение), переименовывать файлы, хранить вне document_root при необходимости.

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

Используйте массивное имя поля: name="files[]" в форме. В PHP $_FILES['files'] будет содержать массивы для каждого атрибута (name, type, tmp_name, error, size).


<form action="upload_multi.php" method="post" enctype="multipart/form-data">
  <input type="file" name="files[]" multiple />
  <input type="submit" />
</form>
<?php
// upload_multi.php
$files = $_FILES['files'];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
    if ($files['error'][$i] === UPLOAD_ERR_OK) {
        $tmp = $files['tmp_name'][$i];
        $name = basename($files['name'][$i]);
        move_uploaded_file($tmp, 'uploads/' . $name);
    }
}
?>
  

Проблема: При большом количестве файлов может превысить лимит времени выполнения или памяти. Решение - установить больший лимит или обрабатывать файлы асинхронно.

Как проверить и ограничить тип файла по MIME?

Используйте функцию finfo (Fileinfo) вместо проверки расширения, так как расширение может быть подделано.


$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mime, $allowed)) {
    die('Недопустимый тип файла.');
}
  

Ошибка: Расширение Fileinfo может быть отключено в php.ini. Включите его или используйте mime_content_type() (менее надёжно).

Как обработать ошибки загрузки?

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


$errorCode = $_FILES['file']['error'];
$messages = [
    UPLOAD_ERR_OK         => 'Файл загружен успешно.',
    UPLOAD_ERR_INI_SIZE   => 'Размер файла превышает максимальный размер, заданный в php.ini.',
    UPLOAD_ERR_FORM_SIZE  => 'Размер файла превышает максимальный размер, заданный в форме.',
    UPLOAD_ERR_PARTIAL    => 'Файл загружен не полностью.',
    UPLOAD_ERR_NO_FILE    => 'Файл не выбран.',
    UPLOAD_ERR_NO_TMP_DIR => 'Отсутствует временная папка.',
    UPLOAD_ERR_CANT_WRITE => 'Не удалось записать файл на диск.',
];
echo $messages[$errorCode] ?? 'Неизвестная ошибка.';
  

Без обработки ошибок пользователь может не понять, почему загрузка не удалась. Всегда анализируйте код ошибки.

Как переименовать файл во избежание коллизий?

Создайте уникальное имя, например, с помощью uniqid() или хеша от текущего времени.


$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = uniqid('file_', true) . '.' . $ext;
$dest = 'uploads/' . $newName;
move_uploaded_file($file['tmp_name'], $dest);
  

Стандартное имя может перезаписать существующий файл. Использование уникального имени решает проблему.

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

Используйте объект FormData в JavaScript. Это позволяет отправить файл без перезагрузки страницы.


// client.js
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
    e.preventDefault();
    const data = new FormData(form);
    fetch('upload.php', {
        method: 'POST',
        body: data
    })
    .then(response => response.text())
    .then(console.log);
});
  

<!-- upload.php (тот же, что и в основном решении) -->
  

Не забудьте установить правильные заголовки CORS, если скрипт находится на другом домене. Также в некоторых старых браузерах FormData не поддерживается.

Как защититься от атак при загрузке файлов?

Не доверяйте расширению и MIME-типу от клиента. Проверяйте содержимое через finfo, переименовывайте файлы, храните за пределами document_root, отключите выполнение PHP в папке uploads через .htaccess.


// Пример .htaccess для папки uploads
<FilesMatch \.php$>
    Order Deny,Allow
    Deny from all
</FilesMatch>
  

Если файлы хранятся в web-доступной папке, злоумышленник может загрузить PHP-шелл. Проверка расширения и двойного расширения (например, image.php.jpg) недостаточна. Используйте finfo и переименование с заменой расширения на безопасное (например, только .jpg, .png).

Расширенные примеры загрузки файлов

1. Загрузка одного файла с полной проверкой и логированием

Пример

<?php
function uploadFile(array $file, string $targetDir, array $allowedMimes, int $maxSize): string
{
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Ошибка загрузки: ' . $file['error']);
    }
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, $allowedMimes)) {
        throw new RuntimeException('Недопустимый MIME-тип: ' . $mime);
    }
    if ($file['size'] > $maxSize) {
        throw new RuntimeException('Размер файла превышает ' . ($maxSize / 1024 / 1024) . ' MB');
    }
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $newName = md5(uniqid(mt_rand(), true)) . '.' . $ext;
    $dest = $targetDir . '/' . $newName;
    if (!move_uploaded_file($file['tmp_name'], $dest)) {
        throw new RuntimeException('Не удалось переместить файл');
    }
    return $dest;
}

try {
    $path = uploadFile($_FILES['file'], __DIR__ . '/uploads', ['image/jpeg', 'image/png'], 2*1024*1024);
    echo "Файл сохранён: " . htmlspecialchars($path);
} catch (RuntimeException $e) {
    echo 'Ошибка: ' . $e->getMessage();
}
?>
// Результат (успех):
Файл сохранён: /var/www/uploads/a1b2c3d4e5f6.jpg
// Результат (ошибка):
Ошибка: Недопустимый MIME-тип: application/x-php

2. Загрузка нескольких файлов с переименованием и статистикой

Пример

<?php
$targetDir = __DIR__ . '/uploads';
$files = $_FILES['files'];
$success = 0;
$errors = [];
for ($i = 0; $i < count($files['name']); $i++) {
    if ($files['error'][$i] === UPLOAD_ERR_OK) {
        $ext = pathinfo($files['name'][$i], PATHINFO_EXTENSION);
        $newName = uniqid() . '.' . $ext;
        $dest = $targetDir . '/' . $newName;
        if (move_uploaded_file($files['tmp_name'][$i], $dest)) {
            $success++;
        } else {
            $errors[] = 'Не удалось сохранить ' . $files['name'][$i];
        }
    } else {
        $errors[] = 'Ошибка загрузки ' . $files['name'][$i] . ': код ' . $files['error'][$i];
    }
}
echo "Загружено успешно: $success";
if ($errors) {
    echo "<br>Ошибки:<br>" . implode('<br>', $errors);
}
?>
Загружено успешно: 3
Ошибки:
Ошибка загрузки readme.txt: код 4 (файл не выбран)

3. Определение MIME-типа с помощью finfo и mime_content_type (резервный метод)

Пример

<?php
$filePath = $_FILES['file']['tmp_name'];
if (function_exists('finfo_open')) {
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $filePath);
    finfo_close($finfo);
} elseif (function_exists('mime_content_type')) {
    $mime = mime_content_type($filePath);
} else {
    // fallback: расширение (ненадёжно)
    $ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
    $mime = mime_by_extension($ext); // пользовательская функция
}
echo "Определён MIME: $mime";
?>
Определён MIME: image/png

4. Отправка файла на другой сервер через cURL (прокси-загрузка)

Пример

<?php
$sourceFile = $_FILES['file']['tmp_name'];
$targetUrl = 'https://remote-server.com/upload.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $targetUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
    'file' => new CURLFile($sourceFile, $_FILES['file']['type'], $_FILES['file']['name'])
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo "Ответ сервера: " . htmlspecialchars($response);
?>
Ответ сервера: {"status":"ok","filename":"photo.jpg"}

5. Валидация с помощью Exif (для изображений) и проверка на наличие PHP-кода

Пример

<?php
// Для JPEG дополнительно можно проверить, что файл является изображением
if (function_exists('exif_imagetype')) {
    $imageType = exif_imagetype($_FILES['file']['tmp_name']);
    if (!in_array($imageType, [IMAGETYPE_JPEG, IMAGETYPE_PNG])) {
        die('Файл не является изображением');
    }
}
// Проверка на наличие PHP-тегов (не панацея, но дополнительный барьер)
$content = file_get_contents($_FILES['file']['tmp_name']);
if (stripos($content, '<?php') !== false) {
    die('Файл содержит PHP-код');
}
?>
// Если загружен корректный JPEG, никакого вывода, продолжение скрипта.
// Если загружен файл с PHP-кодом: 'Файл содержит PHP-код'

Отправка файла через PHP - comments

En
отправка файла php (php)