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