En la Unidad 1 el navegador ya validaba: required, type="email", min y max. Esa validación es para el usuario: le avisa rápido que se equivocó. Pero no protege al servidor, porque cualquiera puede saltársela. La validación que cuenta es la que hace PHP.

Por qué no basta el navegador

Incorrecto: confiar en required

Abre F12, busca el <input required>, borra el atributo y envía el formulario vacío. Llega vacío al servidor. Lo mismo se puede hacer sin navegador:

curl -X POST -d "nombre=&correo=noesuncorreo" http://localhost:8000/contacto.php

Todo lo que está en el navegador lo controla el usuario.

Correcto: validar en los dos lados

ValidaciónPara qué¿Se puede saltar?
Navegador (required, type, min)Avisar rápido, sin recargarSí
Servidor (PHP)Asegurar que el dato sirve antes de usarloNo

Se dejan los atributos del HTML y se valida en PHP.

El patrón

Toda validación en el curso sigue los mismos cuatro pasos.

Validar con un arreglo de errores

flowchart TD
    A["POST recibido"] --> B["1. Leer cada campo<br/>con trim y ??"]
    B --> C["2. Revisar cada campo<br/>y anotar el error en $errores"]
    C --> D{"3. ¿$errores<br/>está vacío?"}
    D -->|"sí"| E["4a. Usar los datos<br/>y confirmar"]
    D -->|"no"| F["4b. Volver a mostrar el formulario<br/>con los errores y lo escrito"]
<?php
$errores = [];
$nombre = "";
$correo = "";
 
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // 1. Leer
    $nombre = trim($_POST["nombre"] ?? "");
    $correo = trim($_POST["correo"] ?? "");
 
    // 2. Revisar
    if ($nombre === "") {
        $errores["nombre"] = "Escribe tu nombre.";
    }
 
    if ($correo === "") {
        $errores["correo"] = "Escribe tu correo.";
    } elseif (filter_var($correo, FILTER_VALIDATE_EMAIL) === false) {
        $errores["correo"] = "El correo no tiene un formato válido.";
    }
 
    // 3. ¿Hay errores?
    if (count($errores) === 0) {
        // 4a. Todo bien
    }
}

$errores es un arreglo asociativo: la clave es el name del campo y el valor es el mensaje. Así, junto a cada campo se puede preguntar si tiene un error.

Un error por campo

Para el correo se usa elseif: si está vacío, no tiene sentido decir además que el formato es inválido. Cada campo muestra su primer problema.

Revisar cada tipo de dato

Cómo validar los casos más comunes

Quieres revisarCondición que indica error
Obligatorio$valor === "" (después de trim)
Largo mínimomb_strlen($valor) < 3
Largo máximomb_strlen($valor) > 500
Correofilter_var($valor, FILTER_VALIDATE_EMAIL) === false
Enterofilter_var($valor, FILTER_VALIDATE_INT) === false
Una opción de una lista (select, radio)!in_array($valor, $permitidos, true)
Checkbox marcado (“acepto los términos”)!isset($_POST["acepto"])

Incorrecto: empty para revisar si un campo vino

<?php
$hijos = "0";
 
if (empty($hijos)) {
    echo "Campo obligatorio\n";
}
Campo obligatorio

empty() da true para "", pero también para "0". Si alguien escribe 0 hijos, el campo se rechaza como vacío. Es la trampa de los valores que cuentan como falso.

Correcto: trim y comparar con ""

<?php
$hijos = trim("0");
 
if ($hijos === "") {
    echo "Campo obligatorio\n";
} else {
    echo "Valor recibido: $hijos\n";
}
Valor recibido: 0

Incorrecto: validar un select solo en el HTML

Que el <select> tenga tres opciones no impide que llegue otra: con F12 se puede cambiar el value de una opción. Si el servidor no lo revisa con in_array(), acepta cualquier cosa.

Un formulario completo

contacto.php entero: muestra el formulario, lo procesa, marca los errores en cada campo y conserva lo que se escribió.

<?php
$asuntos = ["consulta", "sugerencia", "reclamo"];
$errores = [];
$enviado = false;
 
$nombre = "";
$correo = "";
$asunto = "";
$mensaje = "";
 
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $nombre = trim($_POST["nombre"] ?? "");
    $correo = trim($_POST["correo"] ?? "");
    $asunto = $_POST["asunto"] ?? "";
    $mensaje = trim($_POST["mensaje"] ?? "");
 
    if ($nombre === "") {
        $errores["nombre"] = "Escribe tu nombre.";
    }
 
    if ($correo === "") {
        $errores["correo"] = "Escribe tu correo.";
    } elseif (filter_var($correo, FILTER_VALIDATE_EMAIL) === false) {
        $errores["correo"] = "El correo no tiene un formato válido.";
    }
 
    if (!in_array($asunto, $asuntos, true)) {
        $errores["asunto"] = "Elige un asunto de la lista.";
    }
 
    if (mb_strlen($mensaje) < 10) {
        $errores["mensaje"] = "El mensaje debe tener al menos 10 caracteres.";
    }
 
    $enviado = count($errores) === 0;
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Contacto</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 p-8 text-gray-800">
  <main class="mx-auto max-w-lg">
    <h1 class="mb-6 text-3xl font-bold">Contacto</h1>
 
    <?php if ($enviado): ?>
      <p class="rounded border border-green-300 bg-green-50 p-4 text-green-800">
        Gracias, <?= htmlspecialchars($nombre) ?>. Te responderemos a <?= htmlspecialchars($correo) ?>.
      </p>
    <?php else: ?>
      <form action="contacto.php" method="post" class="space-y-4" novalidate>
        <div>
          <label for="nombre" class="block font-medium">Nombre</label>
          <input id="nombre" name="nombre" type="text" required
                 value="<?= htmlspecialchars($nombre) ?>"
                 class="w-full rounded border border-gray-300 px-3 py-2">
          <?php if (isset($errores["nombre"])): ?>
            <p class="mt-1 text-sm text-red-700"><?= $errores["nombre"] ?></p>
          <?php endif; ?>
        </div>
 
        <div>
          <label for="correo" class="block font-medium">Correo</label>
          <input id="correo" name="correo" type="email" required
                 value="<?= htmlspecialchars($correo) ?>"
                 class="w-full rounded border border-gray-300 px-3 py-2">
          <?php if (isset($errores["correo"])): ?>
            <p class="mt-1 text-sm text-red-700"><?= $errores["correo"] ?></p>
          <?php endif; ?>
        </div>
 
        <div>
          <label for="asunto" class="block font-medium">Asunto</label>
          <select id="asunto" name="asunto" class="w-full rounded border border-gray-300 px-3 py-2">
            <option value="">Elige un asunto</option>
            <?php foreach ($asuntos as $opcion): ?>
              <?php if ($opcion === $asunto): ?>
                <option value="<?= $opcion ?>" selected><?= ucfirst($opcion) ?></option>
              <?php else: ?>
                <option value="<?= $opcion ?>"><?= ucfirst($opcion) ?></option>
              <?php endif; ?>
            <?php endforeach; ?>
          </select>
          <?php if (isset($errores["asunto"])): ?>
            <p class="mt-1 text-sm text-red-700"><?= $errores["asunto"] ?></p>
          <?php endif; ?>
        </div>
 
        <div>
          <label for="mensaje" class="block font-medium">Mensaje</label>
          <textarea id="mensaje" name="mensaje" rows="4"
                    class="w-full rounded border border-gray-300 px-3 py-2"><?= htmlspecialchars($mensaje) ?></textarea>
          <?php if (isset($errores["mensaje"])): ?>
            <p class="mt-1 text-sm text-red-700"><?= $errores["mensaje"] ?></p>
          <?php endif; ?>
        </div>
 
        <button type="submit" class="rounded bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 focus-visible:outline-2">
          Enviar
        </button>
      </form>
    <?php endif; ?>
  </main>
</body>
</html>
PiezaCómo funciona
Conservar lo escritoCada value (y el contenido del <textarea>) imprime la variable. En la primera visita es ""; si hubo errores, tiene lo que se envió
Conservar la opciónEl <option> que coincide con $asunto recibe selected, igual que en Ciclos que generan HTML
Error junto al campoisset($errores["campo"]) muestra el mensaje debajo del campo
novalidate en el <form>Apaga la validación del navegador solo para probar la de PHP. Quítalo al terminar

Incorrecto: perder lo que escribió el usuario

Si el formulario vuelve vacío por un solo error, el usuario tiene que escribir todo de nuevo. Es la razón número uno por la que la gente abandona un formulario.

Correcto: el error dice qué hacer, no solo qué está mal

“Correo inválido” obliga a adivinar. “El correo no tiene un formato válido” o “Escribe tu correo” explica qué corregir. Y el error va en texto junto al campo, no solo en el color del borde (Interfaces de usuario).

Pruébalo

  • Arma el formulario completo y envíalo vacío: cada campo muestra su error
  • Llena solo el nombre y envía: el nombre se conserva, el resto marca error
  • Escribe 0 en un campo y compara empty() con === ""
  • Con F12, cambia el value de una opción del <select> y comprueba que PHP lo rechaza
  • Error provocado: cambia la clave de un error ($errores["Nombre"]) y comprueba que el mensaje ya no aparece

Errores comunes

SíntomaCausaSolución
Un 0 se rechaza como campo vacíoSe usó empty()trim() y comparar con === ""
El formulario vuelve vacío tras un errorLos value no imprimen las variablesvalue="<?= htmlspecialchars($campo) ?>"
Los errores no aparecen aunque el campo está malLa clave en $errores no coincide con la del issetUsa el name del campo como clave en los dos lugares

Checkpoint

Intenta responder antes de desplegar cada pregunta.