
🚀 Завантаження кількох файлів з використанням 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 із прев’ю, ми розберемо в окремих матеріалах. Напишіть у коментарях, який із них актуальний для вашого проєкту: візьмемо в роботу насамперед.



