
🚀 Subir múltiples archivos usando HTML y PHP
Un usuario necesita subir cinco imágenes, pero el formulario solo acepta una. O un cliente solicita carga de documentos por lotes con un título asociado a cada archivo. Esto no es fantasía: es una tarea estándar que PHP resuelve en treinta líneas.
El problema es que la mayoría de los tutoriales muestran cargas de un solo archivo. La carga múltiple funciona de manera distinta: un array de archivos en lugar de uno, mapeo a campos adicionales, protección contra sobrescritura. A continuación, una implementación funcional desde cero hasta un fragmento listo para usar.
💡 Resumen rápido:
- Cree un formulario HTML con
enctype="multipart/form-data"y un campofilecon el atributomultiple. - Procese el array
$_FILESen PHP: recorra los índices y usemove_uploaded_file()para cada archivo. - Agregue validación de extensión y tamaño antes de guardar en disco.
- Para vincular «archivo + título», use arrays paralelos
title[]yfileUpload[].
Un video rápido que recorre el mismo tema, desde el formulario HTML hasta guardar archivos en una base de datos:
Paso 1: formulario HTML para subir múltiples archivos
Primero, construya el formulario. Puntos clave: el atributo enctype configurado como multipart/form-data y un campo de entrada con multiple. Sin enctype, el servidor simplemente no recibirá archivos; esto no es un error de PHP, sino de la especificación HTTP.
Nombre el campo como un array: fileUpload[]. Los corchetes al final le indican a PHP que el servidor está recibiendo una lista, no un solo archivo.
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>
El formulario anterior permite seleccionar cualquier cantidad de archivos a la vez en el diálogo de archivos. El navegador los empaquetará en una solicitud multiparte y los enviará al servidor. No se necesita JavaScript en esta etapa.
Paso 2: procesamiento PHP del array de archivos
Cuando el formulario se envía al servidor, PHP llena el array superglobal $_FILES. Pero la estructura difiere de las cargas de un solo archivo: las claves name / tmp_name / size / error se convierten en arrays donde el índice corresponde al número secuencial del archivo.
Recorremos los índices y guardamos cada archivo usando 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 // 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 }
Agregue este código al functions.php de su tema hijo o al plugin Code Snippets, y el formulario del paso 1 comenzará a aceptar archivos.
Tenga en cuenta que basename() elimina la ruta del nombre del archivo. Esto protege contra ataques de path traversal: un atacante podría enviar ../../wp-config.php como nombre. basename() deja solo wp-config.php, pero la siguiente capa de defensa debería ser igualmente la validación de extensión.
Paso 3: verificación del tipo y tamaño del archivo
No puede aceptar «tal cual» todo lo que el navegador envía. Protección mínima: una lista blanca de extensiones y un límite de tamaño.
Verificación de extensión usando pathinfo() con el flag PATHINFO_EXTENSION. Convierta a minúsculas y compare con las extensiones permitidas:
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] = 'Invalid file type: ' . $ext; 6 continue; 7 }
La verificación de tamaño es aún más simple; el valor size en bytes ya está en $_FILES:
1 $max_file_size = 5 * 1024 * 1024; // 5 MB 2 3 if ($_FILES['fileUpload']['size'][$key] > $max_file_size) { 4 $errors[$key] = 'File exceeds the allowed size (5 MB)'; 5 continue; 6 }
Coloque ambas verificaciones antes de move_uploaded_file() en el mismo bucle. Los archivos que no pasen la validación no llegarán al disco. Recoja los errores en un array y, después del procesamiento, muestre al usuario una lista indicando qué archivo falló y por qué. Sin esto, los usuarios solo verán un resultado vacío y se irán, asumiendo que el formulario está roto. Para cada archivo rechazado, informe el nombre y el motivo, como «photo.png: tipo no válido» o «archive.zip: tamaño excedido».
Paso 4: renombrar archivos durante la carga
Si dos usuarios suben photo.jpg, el segundo archivo sobrescribirá el primero. La solución es generar un nombre único al guardar. El enfoque más simple: reemplace espacios por guiones bajos y agregue time().
1 if (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 }
La marca de time() proporciona unicidad al segundo. En proyectos de alto tráfico, sustitúyala por uniqid() o UUID: dos solicitudes simultáneas dentro del mismo segundo son suficientes para causar una colisión y perder uno de los archivos. Además, time() revela la fecha de carga en el nombre del archivo, lo que a veces es indeseable. Un UUID de ramsey/uuid elimina completamente ambos problemas.
Paso 5: carga con campos adicionales para cada archivo
A veces necesita adjuntar un título, descripción o categoría a cada archivo. En lugar de un par «título + archivo», crea varios, nombrando los campos como arrays:
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>

Del lado del servidor, $_POST['title'] y $_FILES['fileUpload']['name'] son arrays paralelos. El índice 0 en uno corresponde al índice 0 en el otro. El procesamiento usa el mismo bucle, pero ahora con vinculación de título:
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; // 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 }
Asegúrese de que la cantidad de campos title[] coincida con la cantidad de campos fileUpload[] en el formulario. Si el usuario agrega campos dinámicamente mediante JavaScript, todos los índices deben ser consecutivos y sin huecos. PHP no colapsa automáticamente los arrays dispersos, por lo que $_POST['title'][5] puede existir mientras que $_FILES['fileUpload']['name'][5] no.
En la práctica, este enfoque cubre una buena mitad de las tareas de clientes: galerías de imágenes con leyendas, cargas de documentos con etiquetas, postulaciones con portafolios. El mismo principio de arrays paralelos funciona para descripción, categoría, orden y cualquier otro campo.
⁉️🤔 Preguntas frecuentes
¿Por qué el archivo no aparece en $_FILES aunque el formulario se haya enviado?
Nueve de cada diez veces, falta
enctype="multipart/form-data"en la etiqueta<form>. Sin él, el navegador envía los datos como texto plano y PHP no llena$_FILES. Verifique tambiénpost_max_sizeyupload_max_filesizeenphp.ini; exceder cualquiera de ellos resulta en un array vacío sin errores visibles.
¿Cómo limito el tamaño total de carga para una sola solicitud?
La directiva
post_max_sizeenphp.initrunca todo el cuerpo de la solicitud POST, incluidos archivos y campos de texto. El valor predeterminado según la documentación de PHP es 8 MB. Configúrelo más alto queupload_max_filesizecon cierta holgura para la codificación multiparte (aproximadamente un 20%). Para cargas masivas, incremente ambos parámetros:upload_max_filesize = 20Mypost_max_size = 25M.
¿Se pueden subir archivos de forma asíncrona mediante JavaScript?
Sí. Use
FormDatayfetchconmethod: 'POST'. Agregue camposfileUpload[]usandoformData.append('fileUpload[]', file)para cada archivo seleccionado. No configure manualmente el encabezadoContent-Type; el navegador establecerá automáticamentemultipart/form-datacon el límite correcto. Para retroalimentación visual, use una barra de progreso medianteXMLHttpRequest.upload.onprogress, ya quefetchno expone el progreso directamente.
¿Y la seguridad: un atacante sube un script PHP en lugar de una imagen?
La verificación de extensión del paso 3 es la primera línea de defensa. Segunda: almacene los archivos subidos FUERA de la carpeta raíz del sitio o en un directorio con un
.htaccessque deshabilite la ejecución (php_flag engine off). Tercera: renombre los archivos al guardarlos (paso 4); incluso sishell.phppasa el filtro de extensión, se convierte enshell_1718300000.php, lo cual no es peligroso sin derechos de ejecución. Cuarta: verifique el tipo MIME no desde$_FILES['type'](enviado por el navegador, fácilmente falsificable), sino mediantefinfo(Fileinfo integrado de PHP), que lee la firma del archivo.
Lo que este enfoque proporciona: el panorama completo
Usted ha construido un sistema de carga que maneja tres escenarios de fábrica: archivos por lotes con un botón, carga única con validación y emparejamiento «archivo + metadatos» mediante arrays paralelos. El código ronda las treinta líneas por cada paso, nada extra.
Si trabaja en WordPress, coloque el manejador en el functions.php de su tema hijo o en el plugin Code Snippets. En PHP puro, simplemente ponga el script de procesamiento en el destino de acción del formulario.
El código de este artículo cubre la mayoría de las tareas típicas de carga. Los escenarios restantes (carga fragmentada para archivos grandes, carga directa a almacenamiento compatible con S3 y arrastrar y soltar con vista previa) se tratarán en artículos separados. Escriba en los comentarios cuál es relevante para su proyecto y lo priorizaremos.



