Skip to content

Tutto per WordPress, lo sviluppo web — e non solo

🚀 Caricamento di più file con HTML e PHP

🚀 Caricamento di più file con HTML e PHP

Un utente deve caricare cinque immagini, ma il modulo ne accetta solo una. Oppure un cliente chiede il caricamento batch di documenti con un titolo associato a ciascun file. Non è fantasia: è un'attività standard che PHP risolve in trenta righe.

Il problema è che la maggior parte dei tutorial mostra il caricamento di file singoli. Il multi-upload funziona in modo diverso: un array di file invece di uno solo, mappatura su campi aggiuntivi, protezione contro la sovrascrittura. Di seguito trovi un'implementazione funzionante, da zero fino a uno snippet pronto all'uso.

💡 Panoramica rapida:

  • Crea un modulo HTML con enctype="multipart/form-data" e un campo file con l'attributo multiple.
  • Elabora l'array $_FILES in PHP: itera sugli indici e usa move_uploaded_file() per ciascun file.
  • Aggiungi la validazione di estensione e dimensione prima di salvare su disco.
  • Per associare "file + titolo", usa array paralleli title[] e fileUpload[].

Una breve video-guida sullo stesso argomento, dal modulo HTML al salvataggio dei file in un database:

Step 1: modulo HTML per il caricamento di più file

Per prima cosa, costruisci il modulo. Punti chiave: l'attributo enctype impostato su multipart/form-data e un campo input con multiple. Senza enctype, il server semplicemente non riceverà i file; non è un errore di PHP ma una specifica HTTP.

Assegna al campo un nome in forma di array: fileUpload[]. Le parentesi quadre alla fine indicano a PHP che il server sta ricevendo una lista, non un file singolo.

1<form action="" method="post" enctype="multipart/form-data">
2 <label>Select files:</label>
3 <input type="file" name="fileUpload[]" multiple>
4 <input type="submit" name="Submit" value="Upload">
5</form>

Il modulo qui sopra permette di selezionare un numero qualsiasi di file in una sola volta nella finestra di dialogo. Il browser li impacchetterà in una richiesta multipart e li invierà al server. In questa fase non serve JavaScript.

Step 2: elaborazione PHP dell'array di file

Quando il modulo viene inviato al server, PHP popola il superglobal $_FILES. Ma la struttura è diversa rispetto al caricamento di un file singolo: le chiavi name / tmp_name / size / error diventano array in cui l'indice corrisponde al numero progressivo del file.

Iteriamo sugli indici e salviamo ciascun file usando 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 // Skip empty fields (no file selected)
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}

Aggiungi questo codice al file functions.php del tuo tema child o al plugin Code Snippets, e il modulo dello step 1 inizierà ad accettare file.

Nota che basename() rimuove il percorso dal nome del file. Questo protegge dagli attacchi di path traversal: un malintenzionato potrebbe inviare ../../wp-config.php come nome. basename() lascia solo wp-config.php, ma il livello di difesa successivo deve comunque essere la validazione dell'estensione.

Step 3: controllo del tipo e della dimensione del file

Non puoi accettare "così come sono" tutti i file che il browser invia. Protezione minima: una whitelist di estensioni e un limite di dimensione.

Controllo dell'estensione tramite pathinfo() con il flag PATHINFO_EXTENSION. Converti in minuscolo e confronta con le estensioni consentite:

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] = 'Invalid file type: ' . $ext;
6 continue;
7}

Il controllo della dimensione è ancora più semplice; il valore size in byte è già presente in $_FILES:

1$max_file_size = 5 * 1024 * 1024; // 5 MB
2
3if ($_FILES['fileUpload']['size'][$key] > $max_file_size) {
4 $errors[$key] = 'File exceeds the allowed size (5 MB)';
5 continue;
6}

Inserisci entrambi i controlli prima di move_uploaded_file() nello stesso ciclo. I file che non superano la validazione non arriveranno su disco. Raccogli gli errori in un array e, dopo l'elaborazione, mostra all'utente una lista con l'indicazione di quale file è stato rifiutato e perché. Senza questo passaggio, l'utente vedrà solo un risultato vuoto e se ne andrà, pensando che il modulo sia rotto. Per ogni file rifiutato, riporta il nome e il motivo, ad esempio "photo.png: tipo non valido" o "archive.zip: dimensione superata".

Step 4: rinominare i file durante il caricamento

Se due utenti caricano photo.jpg, il secondo file sovrascriverà il primo. La soluzione è generare un nome univoco al momento del salvataggio. L'approccio più semplice: sostituire gli spazi con underscore e aggiungere time().

1if (isset($_FILES['fileUpload']['name'][$key])
2 && $_FILES['fileUpload']['size'][$key] > 0) {
3
4 $original_filename = $_FILES['fileUpload']['name'][$key];
5
6 // Extension separately
7 $ext = pathinfo($original_filename, PATHINFO_EXTENSION);
8
9 // Name without extension
10 $filename_without_ext = basename($original_filename, '.' . $ext);
11
12 // New name: no spaces + timestamp
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}

Il timestamp time() garantisce unicità al secondo. Su progetti ad alto traffico, sostituiscilo con uniqid() o UUID: due richieste simultanee nello stesso secondo sono sufficienti per causare una collisione e perdere uno dei file. Inoltre, time() rivela la data di caricamento nel nome del file, cosa a volte indesiderata. Un UUID da ramsey/uuid elimina completamente entrambi i problemi.

Step 5: caricamento con campi aggiuntivi per ciascun file

A volte serve associare a ogni file un titolo, una descrizione o una categoria. Invece di una singola coppia "titolo + file", ne crei diverse, nominando i campi come array:

1<form action="" method="post" enctype="multipart/form-data">
2
3 <input type="text" name="title[]" placeholder="Title">
4 <input type="file" name="fileUpload[]">
5
6 <input type="text" name="title[]" placeholder="Title">
7 <input type="file" name="fileUpload[]">
8
9 <input type="text" name="title[]" placeholder="Title">
10 <input type="file" name="fileUpload[]">
11
12 <input type="submit" name="Submit" value="Upload">
13
14</form>
Modulo di caricamento con campi titolo aggiuntivi

Lato server, $_POST['title'] e $_FILES['fileUpload']['name'] sono array paralleli. L'indice 0 nell'uno corrisponde all'indice 0 nell'altro. L'elaborazione usa lo stesso ciclo, ma ora con l'associazione del titolo:

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; // empty field — skip
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 // Save $title and $target to the database
16 // or into an array for further processing
17 }
18}

Assicurati che il numero di campi title[] corrisponda al numero di campi fileUpload[] nel modulo. Se l'utente aggiunge campi dinamicamente via JavaScript, tutti gli indici devono essere consecutivi e senza buchi. PHP non compatta automaticamente gli array sparsi, quindi $_POST['title'][5] potrebbe esistere mentre $_FILES['fileUpload']['name'][5] no.

Nella pratica, questo approccio copre una buona metà delle richieste dei clienti: gallerie di immagini con didascalie, caricamento documenti con tag, invio di curriculum con portfolio. Lo stesso principio degli array paralleli funziona per descrizione, categoria, ordinamento e qualsiasi altro campo.

⁉️🤔 Domande frequenti

Perché il file non compare in $_FILES anche se il modulo è stato inviato?

Nove volte su dieci, manca enctype="multipart/form-data" nel tag <form>. Senza, il browser invia i dati come testo semplice e PHP non popola $_FILES. Verifica anche post_max_size e upload_max_filesize nel php.ini; il superamento di uno dei due produce un array vuoto senza errori visibili.

Come posso limitare la dimensione totale del caricamento per una singola richiesta?

La direttiva post_max_size nel php.ini tronca l'intero corpo della richiesta POST, inclusi file e campi di testo. Il valore predefinito secondo la documentazione PHP è 8 MB. Impostalo più alto di upload_max_filesize con un margine per la codifica multipart (circa il 20%). Per caricamenti massivi, aumenta entrambi i parametri: upload_max_filesize = 20M e post_max_size = 25M.

È possibile caricare file in modo asincrono via JavaScript?

Sì. Usa FormData e fetch con method: 'POST'. Aggiungi i campi fileUpload[] usando formData.append('fileUpload[]', file) per ogni file selezionato. Non impostare manualmente l'header Content-Type; il browser imposterà automaticamente multipart/form-data con il boundary corretto. Per un feedback visivo, usa una barra di progresso tramite XMLHttpRequest.upload.onprogress, poiché fetch non espone direttamente l'avanzamento.

E la sicurezza: un malintenzionato carica uno script PHP invece di un'immagine?

Il controllo dell'estensione dello step 3 è la prima linea di difesa. Seconda: archivia i file caricati FUORI dalla root del sito o in una directory con un .htaccess che disabilita l'esecuzione (php_flag engine off). Terza: rinomina i file al momento del salvataggio (step 4); anche se shell.php supera il filtro dell'estensione, diventa shell_1718300000.php, che non è pericoloso senza diritti di esecuzione. Quarta: verifica il MIME type non da $_FILES['type'] (inviato dal browser, facilmente falsificabile), ma tramite finfo (Fileinfo integrato in PHP), che legge la firma del file.

Cosa offre questo approccio: il quadro completo

Hai costruito un sistema di upload che gestisce tre scenari su due piedi: file multipli con un solo pulsante, caricamento singolo con validazione e abbinamento "file + metadati" tramite array paralleli. Il codice è di circa trenta righe per ogni step, niente di superfluo.

Se lavori in WordPress, inserisci il gestore nel file functions.php del tuo tema child o nel plugin Code Snippets. In PHP puro, metti semplicemente lo script di elaborazione come destinazione dell'action del modulo.

Il codice di questo articolo copre la maggior parte delle tipiche esigenze di upload. Gli scenari restanti (caricamento a blocchi per file grandi, upload diretto su storage compatibile S3 e drag-and-drop con anteprima) saranno trattati in articoli separati. Scrivi nei commenti quale è rilevante per il tuo progetto e gli daremo priorità.