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-код'