Skip to content

Wszystko o WordPressie, tworzeniu stron — i nie tylko

🚀 Przesyłanie wielu plików za pomocą HTML i PHP

🚀 Przesyłanie wielu plików za pomocą HTML i PHP

Użytkownik musi przesłać pięć zdjęć, a formularz przyjmuje tylko jedno. Albo klient prosi o przesyłanie dokumentów paczką z przypisaniem tytułu do każdego pliku. To nie fantastyka: standardowe zadanie, które PHP rozwiązuje w trzydziestu linijkach.

Problem w tym, że większość poradników pokazuje przesyłanie jednego pliku. Multiprzesyłanie działa inaczej: tablica plików zamiast jednego, powiązanie z dodatkowymi polami, ochrona przed nadpisaniem. Poniżej działająca implementacja od zera do gotowego snippetu.

💡 Szybki przegląd:

  • Tworzą Państwo formularz HTML z enctype="multipart/form-data" i polem file z atrybutem multiple.
  • Przetwarzają Państwo tablicę $_FILES w PHP: pętla po indeksach i move_uploaded_file() dla każdego pliku.
  • Dodają Państwo walidację rozszerzenia i rozmiaru przed zapisem na dysk.
  • Dla powiązania „plik + tytuł" używają Państwo równoległych tablic title[] i fileUpload[].

Szybki przegląd wideo tego samego tematu, od formularza HTML do zapisu plików w bazie danych:

Krok 1: Formularz HTML do przesyłania wielu plików

Najpierw proszę zbudować formularz. Kluczowe punkty: atrybut enctype z wartością multipart/form-data i pole wejściowe z multiple. Bez enctype serwer po prostu nie otrzyma plików, to nie błąd PHP, a specyfikacja HTTP.

Nazwę pola proszę podawać jako tablicę: fileUpload[]. Nawiasy kwadratowe na końcu informują PHP, że na serwer przychodzi nie jeden plik, a lista.

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>

Powyższy formularz pozwala zaznaczyć w oknie wyboru dowolną liczbę plików za jednym razem. Przeglądarka sama spakuje je do żądania multipart i wyśle na serwer. Żaden JavaScript na tym etapie nie jest potrzebny.

Krok 2: Przetwarzanie tablicy plików w PHP

Gdy formularz trafia na serwer, PHP wypełnia superglobalną tablicę $_FILES. Jednak struktura jest tam inna niż przy pojedynczym przesyłaniu: klucze name / tmp_name / size / error stają się tablicami, gdzie indeks odpowiada numerowi porządkowemu pliku.

Przechodzimy pętlą po indeksach i zapisujemy każdy plik przez 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}

Proszę dodać ten kod do functions.php motywu potomnego lub do wtyczki Code Snippets, a formularz z kroku 1 zacznie przyjmować pliki.

Proszę zwrócić uwagę: basename() odcina ścieżkę z nazwy pliku. To ochrona przed atakiem path traversal: osoba atakująca może wysłać ../../wp-config.php jako nazwę. basename() zostawia tylko wp-config.php, ale kolejną warstwą ochrony i tak musi być walidacja rozszerzenia.

Krok 3: Sprawdzanie typu i rozmiaru pliku

Nie można przyjmować „jak leci" wszystkiego, co wysłała przeglądarka. Minimalna ochrona: biała lista rozszerzeń i ograniczenie rozmiaru.

Sprawdzanie rozszerzenia przez pathinfo() z flagą PATHINFO_EXTENSION. Proszę zamienić na małe litery i porównać z dozwolonymi:

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}

Rozmiar sprawdza się jeszcze prościej, wartość size w bajtach jest już w $_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}

Obie walidacje proszę umieszczać przed move_uploaded_file() w tej samej pętli. Pliki, które nie przejdą walidacji, nie trafią na dysk. Proszę zbierać błędy do tablicy i po przetworzeniu wyświetlać użytkownikowi listę: który plik i dlaczego nie został przesłany. Bez tego zobaczy on tylko pusty wynik i odejdzie, uznając, że formularz jest zepsuty. Dla każdego odrzuconego pliku proszę podawać nazwę i przyczynę: „photo.png: niedozwolony typ" lub „archive.zip: przekroczono rozmiar".

Krok 4: Zmiana nazw plików podczas przesyłania

Jeśli dwóch użytkowników prześle photo.jpg, drugi plik nadpisze pierwszy. Rozwiązanie: generowanie unikalnej nazwy podczas zapisu. Najprostszy wariant: zamiana spacji na podkreślenia i dodanie 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}

Znacznik time() daje unikalność z dokładnością do sekundy. W projekcie o dużym obciążeniu proszę go zastąpić przez uniqid() lub UUID: dwa jednoczesne żądania w jednej sekundzie wystarczą, by doszło do kolizji i utraty jednego z plików. Ponadto time() ujawnia datę przesłania w nazwie, co czasem jest niepożądane. UUID, na przykład ramsey/uuid, całkowicie eliminuje oba problemy.

Krok 5: Przesyłanie z dodatkowymi polami dla każdego pliku

Zdarza się, że do każdego pliku trzeba dołączyć tytuł, opis lub kategorię. Zamiast jednej pary „tytuł + plik" robią Państwo kilka, nazwy pól formatują Państwo jako tablice:

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>
Formularz przesyłania z dodatkowymi polami tytułu

Po stronie serwera $_POST['title'] i $_FILES['fileUpload']['name'] to równoległe tablice. Indeks 0 w jednej odpowiada indeksowi 0 w drugiej. Przetwarzanie: ta sama pętla, ale teraz z przypisaniem tytułu:

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}

Proszę sprawdzić, czy liczba pól title[] zgadza się z liczbą fileUpload[] w formularzu. Jeśli użytkownik dodał pola dynamicznie przez JavaScript, wszystkie indeksy muszą iść po kolei, bez luk. PHP nie zwija automatycznie tablic rzadkich i wtedy $_POST['title'][5] będzie istnieć, a $_FILES['fileUpload']['name'][5] już nie.

W praktyce to podejście pokrywa dobrą połowę zadań klienckich: galerie zdjęć z podpisami, przesyłanie dokumentów z tagami, składanie CV z portfolio. Ta sama zasada, równoległe tablice, działa dla description, category, order i dowolnych innych pól.

⁉️🤔 Często zadawane pytania

Dlaczego plik nie trafia do $_FILES, mimo że formularz został wysłany?

W dziewięciu przypadkach na dziesięć zapomniano o enctype="multipart/form-data" w tagu <form>. Bez tego przeglądarka wysyła dane jako zwykły tekst i PHP nie wypełnia $_FILES. Proszę sprawdzić również post_max_size i upload_max_filesize w php.ini, przekroczenie któregokolwiek z nich daje pustą tablicę bez widocznych błędów.

Jak ograniczyć całkowity rozmiar przesyłanych danych dla jednego żądania?

Dyrektywa post_max_size w php.ini ogranicza całe ciało żądania POST, włącznie z plikami i polami tekstowymi. Wartość domyślna, zgodnie z dokumentacją PHP, to 8 MB. Proszę ustawić ją wyżej niż upload_max_filesize z zapasem na narzut kodowania multipart (około 20%). Dla masowego przesyłania proszę zwiększyć oba parametry: upload_max_filesize = 20M i post_max_size = 25M.

Czy można przesyłać pliki asynchronicznie przez JavaScript?

Tak. Proszę użyć FormData i fetch z method: 'POST'. Pola fileUpload[] proszę dodawać przez formData.append('fileUpload[]', file) dla każdego wybranego pliku. Nagłówka Content-Type nie należy ustawiać ręcznie, przeglądarka sama ustawi multipart/form-data z poprawną granicą. Do wizualnej informacji zwrotnej nada się pasek postępu przez XMLHttpRequest.upload.onprogress, fetch bezpośrednio nie udostępnia postępu.

Co z bezpieczeństwem: osoba atakująca przesyła skrypt PHP zamiast obrazu?

Sprawdzanie rozszerzenia z kroku 3 to pierwsza linia obrony. Druga: proszę zapisywać przesłane pliki POZA folderem głównym witryny lub w katalogu z .htaccess zabraniającym wykonywania (php_flag engine off). Trzecia: proszę zmieniać nazwy plików podczas zapisu (krok 4), nawet jeśli shell.php przejdzie filtr rozszerzeń, zamieni się w shell_1718300000.php, co samo w sobie nie jest groźne bez prawa do wykonania. Czwarta: proszę sprawdzać typ MIME nie z $_FILES['type'] (wysyła go przeglądarka, łatwo podrobić), a przez finfo (wbudowane w PHP Fileinfo), które odczytuje sygnaturę pliku.

Co daje opisane podejście: obraz podsumowujący

Zbudowali Państwo mechanizm przesyłania, który radzi sobie z trzema scenariuszami od ręki: paczka plików jednym przyciskiem, pojedyncze przesyłanie z walidacją oraz powiązanie „plik + metadane" przez równoległe tablice. Kodu około trzydziestu linijek na każdy krok, nic zbędnego.

Jeśli pracują Państwo w WordPressie, handler proszę umieścić w functions.php motywu potomnego lub we wtyczce Code Snippets. Na czystym PHP wystarczy umieścić skrypt obsługi w target dla action formularza.

Kod z artykułu pokrywa większość typowych zadań związanych z przesyłaniem. Pozostałe scenariusze, chunked-upload dla dużych plików, bezpośrednie przesyłanie do storage kompatybilnego z S3 oraz drag-and-drop z podglądem, omówimy w osobnych materiałach. Proszę napisać w komentarzach, który z nich jest istotny dla Państwa projektu: zajmiemy się nim w pierwszej kolejności.