Skip to content

Tout pour WordPress, le développement web — et plus encore

🚀 Téléversement de plusieurs fichiers avec HTML et PHP

🚀 Téléversement de plusieurs fichiers avec HTML et PHP

Un utilisateur doit téléverser cinq images, mais le formulaire n’en accepte qu’une seule. Ou un client demande un chargement groupé de documents avec un titre associé à chaque fichier. Ce n’est pas de la science-fiction: c’est une tâche standard que PHP résout en trente lignes.

Le problème, c’est que la plupart des tutoriels montrent le téléversement d’un fichier unique. Le multi-upload fonctionne différemment: un tableau de fichiers au lieu d’un seul, une correspondance avec des champs supplémentaires, une protection contre l’écrasement. Voici une implémentation fonctionnelle, de zéro jusqu’à un extrait prêt à l’emploi.

💡 Aperçu rapide:

  • Créez un formulaire HTML avec enctype="multipart/form-data" et un champ file doté de l’attribut multiple.
  • Traitez le tableau $_FILES en PHP: parcourez les indices et utilisez move_uploaded_file() pour chaque fichier.
  • Ajoutez une validation de l’extension et de la taille avant l’enregistrement sur le disque.
  • Pour lier «fichier + titre», utilisez les tableaux parallèles title[] et fileUpload[].

Une rapide démonstration vidéo du même sujet, du formulaire HTML à l’enregistrement des fichiers en base de données:

Étape 1: formulaire HTML pour téléverser plusieurs fichiers

Commencez par construire le formulaire. Points clés: l’attribut enctype positionné à multipart/form-data et un champ de saisie avec multiple. Sans enctype, le serveur ne reçoit tout simplement pas les fichiers; ce n’est pas une erreur PHP, mais une spécification HTTP.

Nommez le champ comme un tableau: fileUpload[]. Les crochets à la fin indiquent à PHP que le serveur reçoit une liste, pas un fichier unique.

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>

Le formulaire ci-dessus permet de sélectionner un nombre quelconque de fichiers en une seule fois dans la boîte de dialogue. Le navigateur les regroupe dans une requête multipart et les envoie au serveur. Aucun JavaScript n’est nécessaire à ce stade.

Étape 2: traitement PHP du tableau de fichiers

Lorsque le formulaire est soumis au serveur, PHP remplit la superglobale $_FILES. Mais la structure diffère de celle d’un téléversement unitaire: les clés name / tmp_name / size / error deviennent des tableaux où l’indice correspond au numéro d’ordre du fichier.

Nous parcourons les indices et enregistrons chaque fichier avec 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}

Ajoutez ce code au fichier functions.php de votre thème enfant ou au plugin Code Snippets, et le formulaire de l’étape 1 commencera à accepter les fichiers.

Notez que basename() supprime le chemin du nom de fichier. Cela protège contre les attaques de traversée de répertoire: un attaquant pourrait envoyer ../../wp-config.php comme nom. basename() ne conserve que wp-config.php, mais la couche de défense suivante doit rester la validation de l’extension.

Étape 3: vérification du type et de la taille du fichier

Vous ne pouvez pas accepter «tel quel» tout ce que le navigateur envoie. Protection minimale: une liste blanche d’extensions et une limite de taille.

Vérification de l’extension avec pathinfo() et le drapeau PATHINFO_EXTENSION. Convertissez en minuscules et comparez avec les extensions autorisées:

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}

Le contrôle de la taille est encore plus simple; la valeur size en octets est déjà dans $_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}

Placez les deux vérifications avant move_uploaded_file() dans la même boucle. Les fichiers qui échouent à la validation n’atteindront pas le disque. Collectez les erreurs dans un tableau et, après le traitement, affichez à l’utilisateur une liste indiquant quel fichier a échoué et pourquoi. Sans cela, les utilisateurs verront simplement un résultat vide et partiront en pensant que le formulaire est cassé. Pour chaque fichier rejeté, indiquez le nom et la raison, par exemple «photo.png: type invalide» ou «archive.zip: taille dépassée».

Étape 4: renommer les fichiers pendant le téléversement

Si deux utilisateurs téléversent photo.jpg, le second fichier écrasera le premier. La solution consiste à générer un nom unique lors de l’enregistrement. L’approche la plus simple: remplacez les espaces par des tirets bas et ajoutez 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}

L’horodatage time() garantit l’unicité à la seconde près. Sur des projets à fort trafic, remplacez-le par uniqid() ou un UUID: deux requêtes simultanées dans la même seconde suffisent à provoquer une collision et à perdre l’un des fichiers. De plus, time() révèle la date de téléversement dans le nom du fichier, ce qui est parfois indésirable. Un UUID issu de ramsey/uuid élimine complètement ces deux problèmes.

Étape 5: téléversement avec des champs supplémentaires pour chaque fichier

Parfois, vous devez associer un titre, une description ou une catégorie à chaque fichier. Au lieu d’une seule paire «titre + fichier», vous en créez plusieurs, en nommant les champs comme des tableaux:

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>
Formulaire de téléchargement avec champs de titre supplémentaires

Côté serveur, $_POST['title'] et $_FILES['fileUpload']['name'] sont des tableaux parallèles. L’indice 0 dans l’un correspond à l’indice 0 dans l’autre. Le traitement utilise la même boucle, mais avec une liaison au titre:

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}

Assurez-vous que le nombre de champs title[] correspond au nombre de champs fileUpload[] dans le formulaire. Si l’utilisateur ajoute des champs dynamiquement via JavaScript, tous les indices doivent être consécutifs, sans trous. PHP ne réduit pas automatiquement les tableaux épars, donc $_POST['title'][5] peut exister alors que $_FILES['fileUpload']['name'][5] n’existe pas.

En pratique, cette approche couvre une bonne moitié des demandes clients: galeries d’images avec légendes, téléversements de documents avec étiquettes, candidatures avec portfolios. Le même principe de tableaux parallèles fonctionne pour la description, la catégorie, l’ordre et tout autre champ.

⁉️🤔 Foire aux questions

Pourquoi le fichier n’apparaît-il pas dans $_FILES alors que le formulaire a bien été soumis?

Neuf fois sur dix, il manque enctype="multipart/form-data" dans la balise <form>. Sans cela, le navigateur envoie les données en texte brut et PHP ne remplit pas $_FILES. Vérifiez aussi post_max_size et upload_max_filesize dans php.ini; dépasser l’un ou l’autre donne un tableau vide, sans erreur visible.

Comment limiter la taille totale de téléversement pour une seule requête?

La directive post_max_size dans php.ini tronque l’intégralité du corps de la requête POST, fichiers et champs texte compris. La valeur par défaut selon la documentation PHP est de 8 Mo. Fixez-la à une valeur supérieure à upload_max_filesize avec une marge pour l’encodage multipart (environ 20%). Pour des chargements groupés, augmentez les deux paramètres: upload_max_filesize = 20M et post_max_size = 25M.

Peut-on téléverser des fichiers de manière asynchrone avec JavaScript?

Oui. Utilisez FormData et fetch avec method: 'POST'. Ajoutez les champs fileUpload[] via formData.append('fileUpload[]', file) pour chaque fichier sélectionné. Ne définissez pas l’en-tête Content-Type manuellement; le navigateur positionnera automatiquement multipart/form-data avec la bonne limite de séparation. Pour un retour visuel, utilisez une barre de progression via XMLHttpRequest.upload.onprogress, car fetch n’expose pas la progression directement.

Qu’en est-il de la sécurité: un attaquant téléverse un script PHP au lieu d’une image?

La vérification de l’extension de l’étape 3 est la première ligne de défense. Deuxième ligne: stockez les fichiers téléversés EN DEHORS du répertoire racine du site ou dans un dossier doté d’un .htaccess qui désactive l’exécution (php_flag engine off). Troisième ligne: renommez les fichiers lors de l’enregistrement (étape 4); même si shell.php passe le filtre d’extension, il devient shell_1718300000.php, ce qui n’est pas dangereux sans droits d’exécution. Quatrième ligne: vérifiez le type MIME non pas depuis $_FILES['type'] (envoyé par le navigateur, facilement falsifiable), mais via finfo (l’extension Fileinfo intégrée à PHP), qui lit la signature du fichier.

Ce que cette approche apporte: le tableau complet

Vous avez construit un système de téléversement qui gère trois scénarios clés en main: des fichiers groupés avec un seul bouton, un téléversement unitaire avec validation, et l’appairage «fichier + métadonnées» via des tableaux parallèles. Le code fait environ trente lignes pour chaque étape, rien de superflu.

Si vous travaillez sous WordPress, placez le gestionnaire dans le functions.php de votre thème enfant ou dans le plugin Code Snippets. En PHP pur, placez simplement le script de traitement à la cible d’action du formulaire.

Le code de cet article couvre la plupart des tâches de téléversement courantes. Les scénarios restants (téléversement fragmenté pour les gros fichiers, envoi direct vers un stockage compatible S3, et glisser-déposer avec aperçu) seront traités dans des articles séparés. Indiquez en commentaire lequel est pertinent pour votre projet, et nous le prioriserons.