Formularios avanzados en PHP: select, radio, checkbox y subida de archivos


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 required o accept.

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

  1. crear un select con cinco provincias;
  2. crear un grupo de radio buttons para elegir nivel de experiencia;
  3. crear checkboxes para seleccionar tres tecnologías;
  4. mostrar en PHP todas las tecnologías elegidas;
  5. mantener seleccionados los campos después de un error;
  6. agregar una biografía opcional con máximo de caracteres;
  7. crear un input de archivo para una imagen;
  8. limitar el archivo a 2 MB;
  9. aceptar solamente JPG y PNG;
  10. guardar el archivo dentro de una carpeta uploads;
  11. crear un formulario de perfil completo con todos los controles vistos;
  12. 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:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Deja un comentario

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo