Веб-разработка: обработка пользовательских файлов в 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