Skip to content

Всё для WordPress, веб-разработки — и не только

🚀 Загрузка нескольких файлов с использованием HTML и PHP

🚀 Загрузка нескольких файлов с использованием 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
5if (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
4if (!in_array($ext, $allowed_ext)) {
5 $errors[$key] = 'Недопустимый тип файла: ' . $ext;
6 continue;
7}

Размер проверяется ещё проще, значение size в байтах уже есть в $_FILES:

1$max_file_size = 5 * 1024 * 1024; // 5 МБ
2
3if ($_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().

1if (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
3for ($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 с превью, мы разберём в отдельных материалах. Напишите в комментариях, какой из них актуален для вашего проекта: возьмём в работу в первую очередь.