En la guía anterior vimos cómo enviar un formulario HTML a PHP utilizando GET y POST, cómo validar datos y cómo volver a mostrar los valores cuando existe un error.
Ahora vamos a trabajar con formularios más completos: select, radio buttons, checkboxes, múltiples valores y carga de archivos. Son controles que aparecen constantemente en registros, perfiles, sistemas administrativos y aplicaciones reales.
Formulario avanzado: qué vamos a construir
Vamos a crear un formulario de perfil con:
- nombre y email;
- provincia mediante
select; - tipo de usuario mediante radio buttons;
- intereses mediante checkboxes;
- biografía con
textarea; - foto de perfil mediante carga de archivo;
- validación del lado del servidor.
El formulario HTML completo
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>Perfil</title> </head> <body> <h1>Completar perfil</h1> <form method="post" enctype="multipart/form-data"> <label for="nombre">Nombre:</label> <input type="text" id="nombre" name="nombre"> <br><br> <label for="email">Email:</label> <input type="email" id="email" name="email"> <br><br> <label for="provincia">Provincia:</label> <select id="provincia" name="provincia"> <option value="">Seleccionar...</option> <option value="la-pampa">La Pampa</option> <option value="cordoba">Córdoba</option> <option value="mendoza">Mendoza</option> </select> <br><br> <p>Tipo de usuario:</p> <label> <input type="radio" name="tipo" value="estudiante"> Estudiante </label> <label> <input type="radio" name="tipo" value="profesional"> Profesional </label> <br><br> <p>Intereses:</p> <label> <input type="checkbox" name="intereses[]" value="php"> PHP </label> <label> <input type="checkbox" name="intereses[]" value="javascript"> JavaScript </label> <label> <input type="checkbox" name="intereses[]" value="mysql"> MySQL </label> <br><br> <label for="bio">Biografía:</label> <textarea id="bio" name="bio" rows="5"></textarea> <br><br> <label for="foto">Foto de perfil:</label> <input type="file" id="foto" name="foto"> <br><br> <button type="submit">Guardar perfil</button> </form> </body> </html>
Select
El elemento select permite elegir una opción de una lista.
<select name="provincia"> <option value="">Seleccionar...</option> <option value="la-pampa">La Pampa</option> <option value="cordoba">Córdoba</option> </select>
Lo que recibe PHP es el atributo value de la opción seleccionada.
$provincia = $_POST['provincia'] ?? '';
Validar un select
No alcanza con comprobar que el valor no esté vacío. También conviene verificar que pertenezca al conjunto permitido.
$provinciasPermitidas = [
'la-pampa',
'cordoba',
'mendoza'
];
if (!in_array($provincia, $provinciasPermitidas, true)) {
$errores[] = 'La provincia seleccionada no es válida';
}
Esto evita confiar ciegamente en lo enviado por el navegador.
Radio buttons
Los radio buttons permiten elegir una sola opción dentro de un grupo.
<label> <input type="radio" name="tipo" value="estudiante"> Estudiante </label> <label> <input type="radio" name="tipo" value="profesional"> Profesional </label>
Ambos comparten el mismo name. Por eso solamente uno puede quedar seleccionado.
$tipo = $_POST['tipo'] ?? '';
Checkboxes
Los checkboxes permiten seleccionar varias opciones.
Para recibir múltiples valores en PHP utilizamos corchetes en el atributo name:
<input type="checkbox" name="intereses[]" value="php"> <input type="checkbox" name="intereses[]" value="javascript"> <input type="checkbox" name="intereses[]" value="mysql">
Si el usuario selecciona PHP y MySQL, PHP puede recibir:
$_POST['intereses'] = [ 'php', 'mysql' ];
Leer varios checkboxes
$intereses = $_POST['intereses'] ?? []; foreach ($intereses as $interes) { echo htmlspecialchars($interes, ENT_QUOTES, 'UTF-8'); }
Validar múltiples valores
Conviene comprobar cada elemento recibido.
$interesesPermitidos = [
'php',
'javascript',
'mysql'
];
foreach ($intereses as $interes) {
if (!in_array($interes, $interesesPermitidos, true)) {
$errores[] = 'Se recibió un interés no válido';
break;
}
}
Mantener un option seleccionado
Cuando el formulario tiene errores, podemos conservar la opción elegida:
<option
value="la-pampa"
<?php echo $provincia === 'la-pampa' ? 'selected' : ''; ?>
>
La Pampa
</option>
Mantener un radio seleccionado
<input
type="radio"
name="tipo"
value="estudiante"
<?php echo $tipo === 'estudiante' ? 'checked' : ''; ?>
>
Mantener checkboxes seleccionados
<input
type="checkbox"
name="intereses[]"
value="php"
<?php echo in_array('php', $intereses, true) ? 'checked' : ''; ?>
>
Textarea
En un textarea el valor no se coloca en un atributo value. Se escribe entre la etiqueta de apertura y la de cierre.
<textarea name="bio"><?php echo htmlspecialchars( $bio, ENT_QUOTES, 'UTF-8' ); ?></textarea>
Carga de archivos
Para subir archivos necesitamos agregar este atributo al formulario:
<form method="post" enctype="multipart/form-data">
Sin multipart/form-data, el archivo no se enviará correctamente.
El input file
<input type="file" name="foto">
Los archivos no llegan mediante $_POST. PHP utiliza otra superglobal:
$_FILES
Qué contiene $_FILES
Para un campo llamado foto podemos tener:
$_FILES['foto'] = [ 'name' => 'perfil.jpg', 'type' => 'image/jpeg', 'tmp_name' => '/tmp/php123.tmp', 'error' => 0, 'size' => 153245 ];
Los datos más importantes son:
name: nombre original;tmp_name: ubicación temporal;error: código de error;size: tamaño en bytes.
Comprobar si la carga fue correcta
if ( isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK ) { echo 'El archivo llegó correctamente'; }
Validar el tamaño
Por ejemplo, podemos establecer un máximo de 2 MB:
$maximo = 2 * 1024 * 1024; if ($_FILES['foto']['size'] > $maximo) { $errores[] = 'La imagen no puede superar los 2 MB'; }
No confiar solamente en la extensión
Un nombre como foto.jpg no garantiza que el archivo realmente sea una imagen.
La validación de archivos requiere especial cuidado. Para una introducción simple podemos comenzar comprobando extensión y tamaño, pero en una aplicación real también conviene verificar el tipo real del archivo.
Obtener la extensión
$extension = strtolower( pathinfo( $_FILES['foto']['name'], PATHINFO_EXTENSION ) );
Validar extensiones permitidas
$permitidas = ['jpg', 'jpeg', 'png']; if (!in_array($extension, $permitidas, true)) { $errores[] = 'Formato de imagen no permitido'; }
Mover el archivo subido
Cuando PHP recibe un archivo, primero lo guarda temporalmente.
Si todo es correcto, podemos moverlo:
$destino = __DIR__ . '/uploads/perfil.' . $extension; if (!move_uploaded_file( $_FILES['foto']['tmp_name'], $destino )) { $errores[] = 'No se pudo guardar la imagen'; }
move_uploaded_file() está diseñada específicamente para archivos recibidos mediante HTTP POST.
Evitar usar directamente el nombre original
No conviene guardar archivos utilizando directamente:
$_FILES['foto']['name']
El nombre puede contener datos inesperados o coincidir con otro archivo existente.
En proyectos reales suele generarse un nombre controlado por la aplicación.
Formulario completo con procesamiento
Ahora vamos a unir todos los conceptos en un archivo llamado perfil.php.
<?php $nombre = ''; $email = ''; $provincia = ''; $tipo = ''; $intereses = []; $bio = ''; $errores = []; $guardado = false; $provinciasPermitidas = [ 'la-pampa', 'cordoba', 'mendoza' ]; $tiposPermitidos = [ 'estudiante', 'profesional' ]; $interesesPermitidos = [ 'php', 'javascript', 'mysql' ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $nombre = trim($_POST['nombre'] ?? ''); $email = trim($_POST['email'] ?? ''); $provincia = $_POST['provincia'] ?? ''; $tipo = $_POST['tipo'] ?? ''; $intereses = $_POST['intereses'] ?? []; $bio = trim($_POST['bio'] ?? ''); if ($nombre === '') { $errores[] = 'El nombre es obligatorio'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errores[] = 'El email no es válido'; } if (!in_array( $provincia, $provinciasPermitidas, true )) { $errores[] = 'Provincia no válida'; } if (!in_array( $tipo, $tiposPermitidos, true )) { $errores[] = 'Tipo de usuario no válido'; } if (!is_array($intereses)) { $intereses = []; $errores[] = 'Los intereses no son válidos'; } foreach ($intereses as $interes) { if (!in_array( $interes, $interesesPermitidos, true )) { $errores[] = 'Interés no válido'; break; } } if ( isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK ) { $maximo = 2 * 1024 * 1024; if ($_FILES['foto']['size'] > $maximo) { $errores[] = 'La imagen no puede superar los 2 MB'; } $extension = strtolower( pathinfo( $_FILES['foto']['name'], PATHINFO_EXTENSION ) ); $extensionesPermitidas = [ 'jpg', 'jpeg', 'png' ]; if (!in_array( $extension, $extensionesPermitidas, true )) { $errores[] = 'Formato de imagen no permitido'; } } if (empty($errores)) { if ( isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK ) { $nombreArchivo = 'perfil_' . time() . '.' . $extension; $destino = __DIR__ . '/uploads/' . $nombreArchivo; if (!move_uploaded_file( $_FILES['foto']['tmp_name'], $destino )) { $errores[] = 'No se pudo guardar la imagen'; } } } if (empty($errores)) { $guardado = true; } } ?> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>Perfil</title> </head> <body> <h1>Completar perfil</h1> <?php if ($guardado): ?> <p>Perfil guardado correctamente.</p> <?php else: ?> <?php if (!empty($errores)): ?> <ul> <?php foreach ($errores as $error): ?> <li> <?php echo htmlspecialchars( $error, ENT_QUOTES, 'UTF-8' ); ?> </li> <?php endforeach; ?> </ul> <?php endif; ?> <form method="post" enctype="multipart/form-data" > <label for="nombre">Nombre:</label> <input type="text" id="nombre" name="nombre" value="<?php echo htmlspecialchars( $nombre, ENT_QUOTES, 'UTF-8' ); ?>" > <br><br> <label for="email">Email:</label> <input type="email" id="email" name="email" value="<?php echo htmlspecialchars( $email, ENT_QUOTES, 'UTF-8' ); ?>" > <br><br> <label for="provincia"> Provincia: </label> <select id="provincia" name="provincia" > <option value=""> Seleccionar... </option> <option value="la-pampa" <?php echo $provincia === 'la-pampa' ? 'selected' : ''; ?> > La Pampa </option> <option value="cordoba" <?php echo $provincia === 'cordoba' ? 'selected' : ''; ?> > Córdoba </option> <option value="mendoza" <?php echo $provincia === 'mendoza' ? 'selected' : ''; ?> > Mendoza </option> </select> <br><br> <p>Tipo de usuario:</p> <label> <input type="radio" name="tipo" value="estudiante" <?php echo $tipo === 'estudiante' ? 'checked' : ''; ?> > Estudiante </label> <label> <input type="radio" name="tipo" value="profesional" <?php echo $tipo === 'profesional' ? 'checked' : ''; ?> > Profesional </label> <br><br> <p>Intereses:</p> <label> <input type="checkbox" name="intereses[]" value="php" <?php echo in_array( 'php', $intereses, true ) ? 'checked' : ''; ?> > PHP </label> <label> <input type="checkbox" name="intereses[]" value="javascript" <?php echo in_array( 'javascript', $intereses, true ) ? 'checked' : ''; ?> > JavaScript </label> <label> <input type="checkbox" name="intereses[]" value="mysql" <?php echo in_array( 'mysql', $intereses, true ) ? 'checked' : ''; ?> > MySQL </label> <br><br> <label for="bio">Biografía:</label> <textarea id="bio" name="bio" rows="5" ><?php echo htmlspecialchars( $bio, ENT_QUOTES, 'UTF-8' ); ?></textarea> <br><br> <label for="foto"> Foto de perfil: </label> <input type="file" id="foto" name="foto" accept=".jpg,.jpeg,.png" > <br><br> <button type="submit"> Guardar perfil </button> </form> <?php endif; ?> </body> </html>
Qué ocurre en este ejemplo
La secuencia es:
1. El usuario abre perfil.php 2. PHP muestra el formulario 3. El usuario completa los campos 4. Selecciona opciones 5. Adjunta una imagen 6. Presiona Guardar perfil 7. El navegador envía POST multipart/form-data 8. Los campos llegan a $_POST 9. El archivo llega a $_FILES 10. PHP valida todos los datos 11. Si hay errores, vuelve a mostrar el formulario 12. Si todo está correcto, mueve el archivo 13. La aplicación continúa con el procesamiento
Campos opcionales
No todos los campos tienen que ser obligatorios.
Por ejemplo, una biografía puede ser opcional:
$bio = trim($_POST['bio'] ?? ''); if ($bio !== '' && strlen($bio) > 500) { $errores[] = 'La biografía no puede superar 500 caracteres'; }
Primero comprobamos si existe contenido y solamente entonces aplicamos la regla adicional.
HTML no es una barrera de seguridad
Podemos escribir:
<input
type="file"
name="foto"
accept=".jpg,.jpeg,.png"
>
pero accept solamente ayuda al usuario a elegir archivos adecuados. La validación real debe realizarse nuevamente en PHP.
Errores comunes
- olvidar
enctype="multipart/form-data"; - intentar leer archivos desde
$_POST; - olvidar los corchetes en checkboxes múltiples;
- confiar en valores de select, radio o checkbox sin validarlos;
- confiar solamente en la extensión del archivo;
- guardar directamente el nombre original;
- no limitar el tamaño de los archivos;
- no conservar las opciones seleccionadas cuando existe un error;
- confiar solamente en atributos HTML como
requiredoaccept.
Buenas prácticas
- validar todos los datos nuevamente en PHP;
- usar listas de valores permitidos;
- tratar los archivos con reglas más estrictas que un campo de texto;
- generar nombres controlados para los archivos;
- limitar tamaño y formatos;
- mostrar mensajes claros al usuario;
- mantener los datos cargados después de un error;
- escapar siempre la salida cuando se imprime dentro de HTML.
Ejercicios
- crear un select con cinco provincias;
- crear un grupo de radio buttons para elegir nivel de experiencia;
- crear checkboxes para seleccionar tres tecnologías;
- mostrar en PHP todas las tecnologías elegidas;
- mantener seleccionados los campos después de un error;
- agregar una biografía opcional con máximo de caracteres;
- crear un input de archivo para una imagen;
- limitar el archivo a 2 MB;
- aceptar solamente JPG y PNG;
- guardar el archivo dentro de una carpeta
uploads; - crear un formulario de perfil completo con todos los controles vistos;
- agregar nuevas reglas de validación al ejemplo final.
Qué aprendimos
- cómo utilizar
select; - cómo trabajar con radio buttons;
- cómo enviar múltiples checkboxes;
- cómo recibir arrays desde un formulario;
- cómo conservar selecciones después de un error;
- cómo procesar un
textarea; - cómo funciona
multipart/form-data; - cómo utilizar
$_FILES; - cómo validar tamaño y extensión;
- cómo mover archivos subidos;
- por qué la validación del servidor sigue siendo obligatoria.
¿Qué sigue?
Con esto ya tenemos una base muy sólida de PHP. El siguiente paso será comenzar con PHP y MySQL: conexión a una base de datos mediante PDO, consultas preparadas y lectura de registros.
¿Te sirvió esta guía? ☕
Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:
Descubre más desde Club Programador
Suscríbete y recibe las últimas entradas en tu correo electrónico.