
🚀 Загрузка нескольких файлов с использованием HTML и PHP
Пользователю нужно загрузить пять изображений, а форма принимает только одно. Или клиент просит загружать документы пакетом с привязкой заголовка к каждому файлу. Это не фантастика: стандартная задача, которую PHP решает в тридцать строк.
Проблема в том, что большинство мануалов показывают загрузку одного файла. Мультизагрузка устроена иначе: массив файлов вместо одного, сопоставление с дополнительными полями, защита от перезаписи. Ниже, рабочая реализация с нуля до готового сниппета.
💡 Быстрый обзор:
- Создаёте HTML-форму с
enctype="multipart/form-data"и полемfileс атрибутомmultiple. - Обрабатываете массив
$_FILESв PHP: цикл по индексам иmove_uploaded_file()для каждого файла. - Добавляете валидацию расширения и размера до сохранения на диск.
- Для связки «файл + заголовок» используете параллельные массивы
title[]иfileUpload[].
Быстрый видеоразбор этой же темы, от HTML-формы до сохранения файлов в базу:
Шаг 1: HTML-форма для загрузки нескольких файлов
Сначала соберите форму. Ключевые моменты: атрибут enctype со значением multipart/form-data и поле ввода с multiple. Без enctype сервер просто не получит файлы, это не ошибка PHP, а спецификация HTTP.
Имя поля задавайте как массив: fileUpload[]. Квадратные скобки на конце дают PHP понять, что на сервер приходит не один файл, а список.
1 <form action="" method="post" enctype="multipart/form-data"> 2 <label>Выберите файлы:</label> 3 <input type="file" name="fileUpload[]" multiple> 4 <input type="submit" name="Submit" value="Загрузить"> 5 </form>
Форма выше позволяет выделить в диалоге выбора любое количество файлов за раз. Браузер сам упакует их в multipart-запрос и отправит на сервер. Никакого JavaScript на этом этапе не нужно.
Шаг 2: PHP-обработка массива файлов
Когда форма уходит на сервер, PHP заполняет суперглобальный массив $_FILES. Но структура там не такая, как при одиночной загрузке: ключи name / tmp_name / size / error становятся массивами, где индекс соответствует порядковому номеру файла.
Проходим циклом по индексам и сохраняем каждый файл через move_uploaded_file():
1 <?php 2 3 $target_dir = 'uploads/'; 4 5 if (isset($_FILES['fileUpload']['name'])) { 6 7 $total_files = count($_FILES['fileUpload']['name']); 8 9 for ($key = 0; $key < $total_files; $key++) { 10 11 // Пропускаем пустые поля (файл не выбран) 12 if (isset($_FILES['fileUpload']['name'][$key]) 13 && $_FILES['fileUpload']['size'][$key] > 0) { 14 15 $original_filename = $_FILES['fileUpload']['name'][$key]; 16 $target = $target_dir . basename($original_filename); 17 $tmp = $_FILES['fileUpload']['tmp_name'][$key]; 18 19 move_uploaded_file($tmp, $target); 20 } 21 } 22 }
Добавьте этот код в functions.php дочерней темы или в плагин Code Snippets, и форма из шага 1 начнёт принимать файлы.
Обратите внимание: basename() отрезает путь из имени файла. Это защита от атаки с подделкой пути (path traversal): злоумышленник может отправить ../../wp-config.php как имя. basename() оставляет только wp-config.php, но следующим слоем защиты всё равно должна быть валидация расширения.
Шаг 3: Проверка типа и размера файла
Принимать «как есть» всё, что прислал браузер, нельзя. Минимальная защита: белый список расширений и потолок размера.
Проверка расширения, через pathinfo() с флагом PATHINFO_EXTENSION. Приводим к нижнему регистру и сверяем с разрешёнными:
1 $allowed_ext = array('jpg', 'jpeg', 'png', 'gif', 'bmp', 'pdf', 'doc', 'docx'); 2 $ext = strtolower(pathinfo($_FILES['fileUpload']['name'][$key], PATHINFO_EXTENSION)); 3 4 if (!in_array($ext, $allowed_ext)) { 5 $errors[$key] = 'Недопустимый тип файла: ' . $ext; 6 continue; 7 }
Размер проверяется ещё проще, значение size в байтах уже есть в $_FILES:
1 $max_file_size = 5 * 1024 * 1024; // 5 МБ 2 3 if ($_FILES['fileUpload']['size'][$key] > $max_file_size) { 4 $errors[$key] = 'Файл превышает допустимый размер (5 МБ)'; 5 continue; 6 }
Обе проверки ставьте до move_uploaded_file() в том же цикле. Файлы, не прошедшие валидацию, не попадут на диск. Собирайте ошибки в массив и после обработки выводите пользователю список: какой файл и почему не загружен. Без этого он просто увидит пустой результат и уйдёт, решив что форма сломана. Для каждого отклонённого файла сообщайте имя и причину, «photo.png: недопустимый тип» или «archive.zip: превышен размер».
Шаг 4: Переименование файлов при загрузке
Если два пользователя загрузят photo.jpg, второй файл перезапишет первый. Решение, генерировать уникальное имя при сохранении. Простейший вариант: заменить пробелы на подчёркивания и добавить time().
1 if (isset($_FILES['fileUpload']['name'][$key]) 2 && $_FILES['fileUpload']['size'][$key] > 0) { 3 4 $original_filename = $_FILES['fileUpload']['name'][$key]; 5 6 // Расширение отдельно 7 $ext = pathinfo($original_filename, PATHINFO_EXTENSION); 8 9 // Имя без расширения 10 $filename_without_ext = basename($original_filename, '.' . $ext); 11 12 // Новое имя: без пробелов + метка времени 13 $new_filename = str_replace(' ', '_', $filename_without_ext) 14 . '_' . time() . '.' . $ext; 15 16 move_uploaded_file( 17 $_FILES['fileUpload']['tmp_name'][$key], 18 $target_dir . $new_filename 19 ); 20 }
Метка time() даёт уникальность с точностью до секунды. На высоконагруженном проекте замените её на uniqid() или UUID: двух одновременных запросов в одну секунду достаточно, чтобы получить коллизию и потерять один из файлов. Также time() раскрывает дату загрузки в имени, иногда это нежелательно. UUID вида ramsey/uuid полностью снимает обе проблемы.
Шаг 5: Загрузка с дополнительными полями на каждый файл
Бывает, что к каждому файлу нужно прикрепить заголовок, описание или категорию. Вместо одной пары «заголовок + файл» делаете несколько, имена полей оформляете как массивы:
1 <form action="" method="post" enctype="multipart/form-data"> 2 3 <input type="text" name="title[]" placeholder="Заголовок"> 4 <input type="file" name="fileUpload[]"> 5 6 <input type="text" name="title[]" placeholder="Заголовок"> 7 <input type="file" name="fileUpload[]"> 8 9 <input type="text" name="title[]" placeholder="Заголовок"> 10 <input type="file" name="fileUpload[]"> 11 12 <input type="submit" name="Submit" value="Загрузить"> 13 14 </form>

На серверной стороне $_POST['title'] и $_FILES['fileUpload']['name'], параллельные массивы. Индекс 0 в одном соответствует индексу 0 в другом. Обработка, тот же цикл, но теперь с привязкой заголовка:
1 $total_files = count($_FILES['fileUpload']['name']); 2 3 for ($key = 0; $key < $total_files; $key++) { 4 5 if ($_FILES['fileUpload']['size'][$key] === 0) { 6 continue; // пустое поле — пропускаем 7 } 8 9 $title = isset($_POST['title'][$key]) ? $_POST['title'][$key] : ''; 10 $tmp = $_FILES['fileUpload']['tmp_name'][$key]; 11 $fname = basename($_FILES['fileUpload']['name'][$key]); 12 $target = $target_dir . time() . '_' . $fname; 13 14 if (move_uploaded_file($tmp, $target)) { 15 // Сохраните $title и $target в базу данных 16 // или в массив для дальнейшей обработки 17 } 18 }
Проверьте, что количество полей title[] совпадает с количеством fileUpload[] в форме. Если пользователь добавил поля динамически через JavaScript, все индексы должны идти подряд, без пропусков. PHP не схлопывает разреженные массивы автоматически, и тогда $_POST['title'][5] будет существовать, а $_FILES['fileUpload']['name'][5], нет.
На практике этот подход закрывает добрую половину клиентских задач: галереи изображений с подписями, загрузку документов с тегами, подачу резюме с портфолио. Тот же принцип, параллельные массивы, работает для description, category, order и любых других полей.
⁉️🤔 Частые вопросы
Почему файл не приходит в $_FILES, хотя форма отправлена?
В девяти случаях из десяти забыт
enctype="multipart/form-data"у тега<form>. Без него браузер отправляет данные как обычный текст, и PHP не заполняет$_FILES. Проверьте такжеpost_max_sizeиupload_max_filesizeвphp.ini, превышение любого из них даёт пустой массив без видимых ошибок.
Как ограничить общий размер загрузки для одного запроса?
Директива
post_max_sizeвphp.iniобрезает всё тело POST-запроса, включая файлы и текстовые поля. Значение по умолчанию согласно документации PHP, 8 МБ. Установите его вышеupload_max_filesizeс запасом на накладные расходы multipart-кодирования (примерно 20%). Для массовой загрузки увеличьте оба параметра:upload_max_filesize = 20Mиpost_max_size = 25M.
Можно ли загружать файлы асинхронно через JavaScript?
Да. Используйте
FormDataиfetchсmethod: 'POST'. ПоляfileUpload[]добавляйте черезformData.append('fileUpload[]', file)для каждого выбранного файла. ЗаголовокContent-Typeне ставьте вручную, браузер сам выставитmultipart/form-dataс правильной границей. Для визуальной обратной связи подойдёт прогресс-бар черезXMLHttpRequest.upload.onprogress,fetchнапрямую прогресс не отдаёт.
Что делать с безопасностью: злоумышленник загружает PHP-скрипт вместо картинки?
Проверка расширения из шага 3, первый рубеж. Второй: храните загруженные файлы ВНЕ корневой папки сайта или в директории с
.htaccess, запрещающим выполнение (php_flag engine off). Третий: переименовывайте файлы при сохранении (шаг 4), даже еслиshell.phpпройдёт фильтр расширений, он превратится вshell_1718300000.php, что само по себе не страшно без права выполнения. Четвёртый: проверяйте MIME-тип не из$_FILES['type'](его присылает браузер, легко подделать), а черезfinfo(встроенный в PHP Fileinfo), он читает сигнатуру файла.
Что даёт описанный подход: итоговая картина
Вы собрали загрузку, которая справляется с тремя сценариями из коробки: пакет файлов одной кнопкой, одиночная загрузка с валидацией и связка «файл + метаданные» через параллельные массивы. Кода, в районе тридцати строк на каждый шаг, ничего лишнего.
Если работаете в WordPress, обработчик кладите в functions.php дочерней темы или в плагин Code Snippets. На чистом PHP, просто разместите скрипт-обработчик в target для action формы.
Код из статьи закрывает большинство типовых задач по загрузке. Оставшиеся сценарии, chunked-upload для больших файлов, прямая загрузка в S3-совместимое хранилище и drag-and-drop с превью, мы разберём в отдельных материалах. Напишите в комментариях, какой из них актуален для вашего проекта: возьмём в работу в первую очередь.



