Contenido
Indicaciones para el alumnado
Copia cada bloque en un archivo con el nombre indicado. La estructura HTML ya está preparada. Escribe PHP en los lugares marcados con TODO; no cambies los nombres de los campos del formulario. Usa $_POST para leer los datos y htmlspecialchars() al mostrarlos en HTML. Prueba también qué ocurre si envías campos vacíos. Puedes consultar la documentación y usar IA, pero debes poder explicar cada línea que añadas.
Ejercicio 1 — Recibir y mostrar un nombre (01_saludo.php)
Objetivo: detectar el envío, leer un campo y mostrar una respuesta segura. Al abrir la página por primera vez no debe aparecer un saludo; si se envía un nombre vacío debe aparecer un aviso.
<?php
$nombre = '';
$mensaje = '';
// TODO 1: Comprueba que la petición utiliza el método POST.
// Dentro de esa condición, lee $_POST['nombre'].
// Elimina los espacios al principio y al final con trim().
// Si falta el campo, utiliza una cadena vacía como valor inicial.
// TODO 2: Si $nombre está vacío, guarda en $mensaje:
// "Escribe tu nombre". Si no, crea un saludo usando $nombre.
// No introduzcas HTML escrito por el usuario sin escapar.
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ejercicio 1: saludo</title>
</head>
<body>
<h1>Saludo</h1>
<form method="post" action="">
<label for="nombre">Nombre:</label>
<input id="nombre" name="nombre" type="text">
<button type="submit">Enviar</button>
</form>
<?php // TODO 3: Muestra $mensaje dentro de un párrafo SOLO si contiene texto.
// Escapa el contenido al imprimirlo en HTML. ?>
</body>
</html>
Pruebas: Ana → saludo; → aviso; <script>alert(1)</script> → se ve como texto, sin ejecutarse.
Ejercicio 2 — Validar edad y conservar los datos (02_edad.php)
Objetivo: recoger dos campos, mostrar errores y conservar el nombre y la edad cuando el formulario se envía. Edad válida: entero de 0 a 120. El formulario también debe funcionar al abrirse sin enviar datos.
<?php
$nombre = '';
$edad = '';
$errores = [];
$resultado = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee nombre y edad de $_POST con ?? ''.
// Aplica trim() a ambas cadenas.
// TODO 2: Si el nombre está vacío, añade un mensaje a $errores.
// Valida la edad como entero entre 0 y 120.
// Pista: filter_var($edad, FILTER_VALIDATE_INT,
// ['options' => ['min_range' => 0, 'max_range' => 120]])
// puede devolver 0, que es una edad válida: compara con === false.
// TODO 3: Solo si $errores está vacío, crea $resultado:
// "NOMBRE tiene EDAD años" (con los valores enviados).
}
?>
<!doctype html>
<html lang="es">
<head><meta charset="UTF-8"><title>Ejercicio 2: edad</title></head>
<body>
<h1>Registro sencillo</h1>
<form method="post" action="">
<label>Nombre:
<input name="nombre" value="<?= htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') ?>">
</label>
<label>Edad:
<input name="edad" value="<?= htmlspecialchars($edad, ENT_QUOTES, 'UTF-8') ?>">
</label>
<button type="submit">Comprobar</button>
</form>
<?php // TODO 4: Recorre $errores y muestra cada uno en un <p>.
// Muestra $resultado si no está vacío.
// Escapa cualquier dato mostrado en HTML. ?>
</body>
</html>
Pruebas: edad 0 válida; 17 válida; -1, 121, 3.5, abc y campo vacío → error.
Ejercicio 3 — Elegir un curso (03_curso.php)
Objetivo: trabajar con un <select>, una lista de opciones admitidas y un resumen. Evita aceptar un valor enviado manualmente que no figure entre los cursos.
<?php
$cursos = [
'php' => 'PHP',
'linux' => 'Linux',
'redes' => 'Redes'
];
$nombre = '';
$curso = '';
$mensaje = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee y recorta nombre y curso desde $_POST.
// Usa ?? '' para los campos que puedan faltar.
// TODO 2: Comprueba que el nombre no está vacío y que la clave
// $curso existe en $cursos (array_key_exists()).
// Si algún dato falla, guarda un mensaje de error.
// Si ambos son válidos, guarda un resumen con el nombre
// del alumno y el nombre legible del curso.
}
?>
<!doctype html>
<html lang="es">
<head><meta charset="UTF-8"><title>Ejercicio 3: curso</title></head>
<body>
<h1>Inscripción</h1>
<form method="post" action="">
<label>Nombre:
<input name="nombre" value="<?= htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') ?>">
</label>
<label>Curso:
<select name="curso">
<option value="">Selecciona uno</option>
<?php foreach ($cursos as $codigo => $etiqueta): ?>
<option value="<?= htmlspecialchars($codigo, ENT_QUOTES, 'UTF-8') ?>"
<?= $curso === $codigo ? 'selected' : '' ?>>
<?= htmlspecialchars($etiqueta, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</label>
<button type="submit">Inscribirse</button>
</form>
<?php // TODO 3: Muestra $mensaje si contiene texto. Escápalo. ?>
</body>
</html>
Pruebas: el curso elegido permanece seleccionado después de enviar; sin nombre o sin curso aparece error; un valor de curso inexistente se rechaza.
Ejercicio 4 — Casillas y cálculo de total (04_pedido.php)
Objetivo: leer varias casillas con el mismo nombre, validar los códigos recibidos y sumar los precios. Si no se marca ninguna casilla, el pedido se rechaza.
<?php
$productos = [
'teclado' => ['nombre' => 'Teclado', 'precio' => 25],
'raton' => ['nombre' => 'Ratón', 'precio' => 15],
'alfombrilla' => ['nombre' => 'Alfombrilla', 'precio' => 10]
];
$seleccion = [];
$error = '';
$total = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Obtén $_POST['productos'] ?? [].
// Verifica que es un array; si no, guarda un error.
// TODO 2: Si es un array, comprueba que se marcó al menos
// un producto y que TODOS sus códigos existen en $productos.
// Para validar cada código, puedes usar is_string() y
// array_key_exists(). Si hay uno incorrecto, muestra error.
// TODO 3: Solo si no hay errores, suma el precio de cada
// producto elegido y guarda el resultado en $total.
// Pista: comienza $total en 0 antes de sumar.
}
?>
<!doctype html>
<html lang="es">
<head><meta charset="UTF-8"><title>Ejercicio 4: pedido</title></head>
<body>
<h1>Pedido de material</h1>
<form method="post" action="">
<?php foreach ($productos as $codigo => $datos): ?>
<label>
<input type="checkbox" name="productos[]"
value="<?= htmlspecialchars($codigo, ENT_QUOTES, 'UTF-8') ?>"
<?= in_array($codigo, $seleccion, true) ? 'checked' : '' ?>>
<?= htmlspecialchars($datos['nombre'], ENT_QUOTES, 'UTF-8') ?>:
<?= $datos['precio'] ?> €
</label><br>
<?php endforeach; ?>
<button type="submit">Calcular</button>
</form>
<?php // TODO 4: Muestra $error si existe o, si el pedido
// es válido, muestra "Total: ... €".
// Recuerda que $total puede valer 0; usa !== null. ?>
</body>
</html>
Pruebas: teclado + ratón → 40 €; todos → 50 €; ninguno → error. Prueba también qué ocurre si se envía manualmente un código de producto desconocido.
Qué explicar en la entrega
Para cada ejercicio, escribe dos o tres frases: qué datos recibe el servidor, qué validación aplicaste y qué comportamiento observaste en las pruebas. No entregues solo una captura del código.
Ejercicio 5 — Buscar en una lista con GET (05_busqueda.php)
Objetivo: entender los parámetros de la URL. Muestra solo las asignaturas cuyo nombre contiene el texto buscado, sin distinguir mayúsculas y minúsculas.
<?php
$asignaturas = ['Fundamentos de Hardware', 'Redes', 'Sistemas Operativos', 'PHP', 'Seguridad'];
$busqueda = '';
$coincidencias = [];
$buscado = isset($_GET['q']);
if ($buscado) {
// TODO 1: Lee $_GET['q'] con ?? '' y quita espacios con trim().
// Si no está vacío, recorre $asignaturas y añade a $coincidencias
// las que contengan el texto; pista: stripos() !== false.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Buscar asignatura</title></head>
<body>
<h1>Buscar asignaturas</h1>
<form method="get" action="">
<input name="q" value="<?= htmlspecialchars($busqueda, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Buscar</button>
</form>
<?php if ($buscado): ?>
<?php // TODO 2: Si la búsqueda está vacía, pide que se escriba algo.
// Si no hay coincidencias, muestra "Sin resultados".
// Si las hay, imprímelas en una lista <ul>, escapando cada nombre. ?>
<?php endif; ?>
</body></html>
Pruebas: busca red, PHP, xxx y una cadena vacía. Observa cómo cambia la URL.
Ejercicio 6 — Calculadora de dos números (06_calculadora.php)
Objetivo: validar valores numéricos y elegir una operación sin aceptar operaciones desconocidas. Muestra los errores y el resultado solamente tras enviar el formulario.
<?php
$a = '';
$b = '';
$operacion = 'sumar';
$error = '';
$resultado = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee a, b y operacion usando $_POST y ?? ''.
// Comprueba con is_numeric() que a y b son números.
// Comprueba que operacion es "sumar", "restar" o "dividir".
// TODO 2: Si la operación es dividir y b vale 0, guarda un error.
// Si todo es válido, calcula y guarda $resultado.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Calculadora</title></head>
<body>
<h1>Calculadora</h1>
<form method="post" action="">
<input name="a" value="<?= htmlspecialchars((string) $a, ENT_QUOTES, 'UTF-8') ?>">
<select name="operacion">
<option value="sumar" <?= $operacion === 'sumar' ? 'selected' : '' ?>>Sumar</option>
<option value="restar" <?= $operacion === 'restar' ? 'selected' : '' ?>>Restar</option>
<option value="dividir" <?= $operacion === 'dividir' ? 'selected' : '' ?>>Dividir</option>
</select>
<input name="b" value="<?= htmlspecialchars((string) $b, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Calcular</button>
</form>
<?php // TODO 3: Muestra $error si existe; en otro caso, muestra
// $resultado si es distinto de null. El resultado puede ser 0. ?>
</body></html>
Pruebas: 8 / 2 → 4; 5 - 5 → 0; 8 / 0 → error; hola + 2 → error.
Ejercicio 7 — Opción de entrega con radio (07_entrega.php)
Objetivo: leer un botón de opción, validar que pertenece a los valores admitidos y conservar la selección.
<?php
$formas = ['recogida' => 'Recogida en tienda', 'envio' => 'Envío a domicilio'];
$seleccion = '';
$mensaje = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee $_POST['entrega'] ?? ''.
// Comprueba con array_key_exists() que es una clave de $formas.
// Si no lo es, guarda un error; si lo es, crea el mensaje
// "Forma de entrega: ..." con la etiqueta correspondiente.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Entrega</title></head>
<body>
<h1>Elige la entrega</h1>
<form method="post" action="">
<?php foreach ($formas as $codigo => $texto): ?>
<label>
<input type="radio" name="entrega"
value="<?= htmlspecialchars($codigo, ENT_QUOTES, 'UTF-8') ?>"
<?= $seleccion === $codigo ? 'checked' : '' ?>>
<?= htmlspecialchars($texto, ENT_QUOTES, 'UTF-8') ?>
</label>
<?php endforeach; ?>
<button type="submit">Confirmar</button>
</form>
<?php // TODO 2: Muestra $mensaje, si existe, en un párrafo escapado. ?>
</body></html>
Pruebas: ninguna opción → error; una opción → mensaje y opción conservada; valor desconocido enviado manualmente → error.
Ejercicio 8 — Etiquetas con varias casillas (08_etiquetas.php)
Objetivo: recibir un array de opciones, comprobar sus valores y mostrar las etiquetas elegidas en una lista.
<?php
$opciones = ['php' => 'PHP', 'sql' => 'SQL', 'html' => 'HTML', 'linux' => 'Linux'];
$elegidas = [];
$error = '';
$validas = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee $_POST['etiquetas'] ?? [].
// Si no es un array, deja $elegidas vacío y guarda un error.
// Si es un array, cópialo a $elegidas.
// TODO 2: Exige al menos una etiqueta. Recorre $elegidas y
// comprueba que cada elemento es string y clave de $opciones.
// Si todo está bien, establece $validas en true.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Etiquetas</title></head>
<body>
<h1>Tecnologías que conoces</h1>
<form method="post" action="">
<?php foreach ($opciones as $codigo => $etiqueta): ?>
<label>
<input type="checkbox" name="etiquetas[]"
value="<?= htmlspecialchars($codigo, ENT_QUOTES, 'UTF-8') ?>"
<?= in_array($codigo, $elegidas, true) ? 'checked' : '' ?>>
<?= htmlspecialchars($etiqueta, ENT_QUOTES, 'UTF-8') ?>
</label>
<?php endforeach; ?>
<button type="submit">Enviar</button>
</form>
<?php // TODO 3: Si hay $error, muéstralo. Si $validas es true,
// crea una lista <ul> con los nombres de $opciones elegidas.
// Escapa los textos al imprimirlos. ?>
</body></html>
Pruebas: ninguna, una y varias opciones; un código que no existe debe producir error.
Ejercicio 9 — Comentario con límite de longitud (09_comentario.php)
Objetivo: procesar un <textarea> y contar caracteres. Un comentario válido tiene entre 10 y 300 caracteres tras quitar espacios en los extremos.
<?php
$comentario = '';
$error = '';
$guardado = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee $_POST['comentario'] ?? '' y usa trim().
// Calcula su longitud con mb_strlen() si está disponible.
// Si no lo está, utiliza strlen() e indica al profesor
// que esta alternativa cuenta bytes en textos UTF-8.
// TODO 2: Si la longitud queda fuera de 10..300, guarda
// un error. Si es válida, establece $guardado en true.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Comentario</title></head>
<body>
<h1>Deja un comentario</h1>
<form method="post" action="">
<label for="comentario">Comentario:</label><br>
<textarea id="comentario" name="comentario" rows="5" cols="45"><?= htmlspecialchars($comentario, ENT_QUOTES, 'UTF-8') ?></textarea><br>
<button type="submit">Enviar</button>
</form>
<?php // TODO 3: Muestra el error o, si $guardado es true,
// muestra el comentario dentro de un <p>.
// Escapa antes de imprimir y utiliza nl2br() si quieres
// conservar los saltos de línea visualmente. ?>
</body></html>
Pruebas: 9, 10, 300 y 301 caracteres; añade un salto de línea y texto con <b>.
Ejercicio 10 — Comprobar una dirección de correo (10_correo.php)
Objetivo: validar un correo sin enviarlo ni guardarlo. Muestra también el nombre del dominio tras la @ cuando sea válido.
<?php
$correo = '';
$error = '';
$dominio = '';
$valido = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee y recorta $_POST['correo'] ?? ''.
// Valida con filter_var($correo, FILTER_VALIDATE_EMAIL).
// Si falla, establece $error.
// TODO 2: Si es válido, separa el correo por '@' con explode()
// y guarda el dominio en $dominio. Activa $valido.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Correo</title></head>
<body>
<h1>Comprobar correo</h1>
<form method="post" action="">
<input name="correo" type="text"
value="<?= htmlspecialchars($correo, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Comprobar</button>
</form>
<?php // TODO 3: Muestra $error o "Correo válido. Dominio: ...".
// Escapa el dominio antes de mostrarlo. ?>
</body></html>
Pruebas: vacío, ana, ana@ejemplo.test y un correo con espacios alrededor.
Ejercicio 11 — Precio con descuento mediante función (11_descuento.php)
Objetivo: crear una función propia y llamarla desde el procesamiento del formulario. Porcentaje admitido: de 0 a 100.
<?php
// TODO 1: Crea la función aplicarDescuento(float $precio, float $porcentaje): float.
// Debe devolver el precio después de restarle el porcentaje indicado.
$precio = '';
$porcentaje = '';
$error = '';
$precioFinal = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 2: Lee precio y porcentaje desde $_POST con ?? ''.
// Verifica que ambos son numéricos. Precio no puede ser negativo;
// porcentaje debe estar entre 0 y 100.
// Si hay un dato incorrecto, guarda un mensaje en $error.
// TODO 3: Cuando los datos sean válidos, convierte a float,
// llama a aplicarDescuento() y guarda $precioFinal.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Descuento</title></head>
<body>
<h1>Calcular descuento</h1>
<form method="post" action="">
<label>Precio: <input name="precio" value="<?= htmlspecialchars((string) $precio, ENT_QUOTES, 'UTF-8') ?>"></label>
<label>Descuento (%): <input name="porcentaje" value="<?= htmlspecialchars((string) $porcentaje, ENT_QUOTES, 'UTF-8') ?>"></label>
<button type="submit">Calcular</button>
</form>
<?php // TODO 4: Muestra el error o el precio final con
// number_format($precioFinal, 2, ',', '.') cuando no sea null. ?>
</body></html>
Pruebas: 100 y 20 → 80,00; 100 y 100 → 0,00; descuento 101 o precio negativo → error.
Ejercicio 12 — Guardar una nota en un archivo (12_notas.php)
Objetivo: añadir notas a un archivo de texto y volver a leerlas. Usa una nota por línea y evita que los saltos de línea del formulario creen entradas adicionales.
<?php
$ruta = __DIR__ . '/notas.txt';
$nota = '';
$mensaje = '';
$contenido = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Lee $_POST['nota'] ?? '' y aplica trim().
// Rechaza el texto vacío. Sustituye \r y \n por espacios
// antes de guardar, para mantener una nota por línea.
// TODO 2: Añade la nota al archivo con file_put_contents(),
// FILE_APPEND y LOCK_EX; añade PHP_EOL al final.
// Comprueba el resultado con === false. Si falla, muestra error;
// si funciona, muestra "Nota guardada".
}
// TODO 3: Si notas.txt existe, lee su contenido con file_get_contents().
// Si la lectura falla, muestra un error y deja $contenido vacío.
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Notas</title></head>
<body>
<h1>Bloc de notas</h1>
<form method="post" action="">
<label>Nota: <input name="nota" value="<?= htmlspecialchars($nota, ENT_QUOTES, 'UTF-8') ?>"></label>
<button type="submit">Guardar</button>
</form>
<?php // TODO 4: Muestra $mensaje y $contenido con HTML escapado.
// Para que los saltos del archivo se vean, puedes usar nl2br()
// DESPUÉS de htmlspecialchars(). ?>
</body></html>
Pruebas: guarda dos notas, recarga la página y verifica que siguen ahí; envía una nota vacía. El directorio donde está 12_notas.php debe permitir la escritura del archivo notas.txt.
Ejercicio 13 — Contar visitas con sesión (13_sesion.php)
Objetivo: usar $_SESSION para mantener un contador entre peticiones y reiniciarlo desde un formulario.
<?php
session_start(); // Debe ejecutarse antes de enviar HTML.
// TODO 1: Si no existe $_SESSION['visitas'], inicialízala en 0.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 2: Si $_POST['accion'] ?? '' es "reiniciar",
// pon $_SESSION['visitas'] a 0.
} else {
// TODO 3: Incrementa el contador en cada carga GET.
}
$visitas = $_SESSION['visitas'] ?? 0;
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Sesión</title></head>
<body>
<h1>Contador de visitas</h1>
<p>Visitas: <?= (int) $visitas ?></p>
<form method="post" action="">
<input type="hidden" name="accion" value="reiniciar">
<button type="submit">Reiniciar</button>
</form>
</body></html>
Pruebas: recarga tres veces, reinicia y vuelve a recargar. Explica qué cuenta el ejercicio: cargas GET dentro de la misma sesión, no usuarios distintos.
Ejercicio 14 — Subir una imagen con $_FILES (14_subida.php)
Objetivo: recibir un fichero de un formulario, revisar el error de subida y guardarlo de forma segura. Solo se admiten imágenes JPEG o PNG de 2 MB como máximo. Este ejercicio requiere que PHP pueda crear la carpeta subidas junto al archivo.
<?php
$mensaje = '';
$nombreGuardado = '';
$directorio = __DIR__ . '/subidas';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// TODO 1: Comprueba que existe $_FILES['imagen'] y que
// $_FILES['imagen']['error'] === UPLOAD_ERR_OK.
// Si falta el archivo o PHP informa de otro error, guarda
// un mensaje de error y no intentes moverlo.
// TODO 2: Si no hubo error, comprueba que 'size' es mayor
// que 0 y no supera 2 * 1024 * 1024 bytes.
// Obtén la ruta temporal desde $_FILES['imagen']['tmp_name'].
// NO confíes en $_FILES['imagen']['type'] ni en la extensión
// original: detecta el MIME real con
// (new finfo(FILEINFO_MIME_TYPE))->file($rutaTemporal).
// Acepta solo image/jpeg e image/png.
// TODO 3: Si el archivo es válido, crea $directorio con
// mkdir($directorio, 0755, true) cuando no exista.
// Comprueba también que la carpeta existe antes de mover.
// Genera un nombre nuevo con bin2hex(random_bytes(16)):
// añade '.jpg' para JPEG o '.png' para PNG.
// NO uses el nombre original enviado por el navegador.
// TODO 4: Mueve el fichero con move_uploaded_file()
// a $directorio . '/' . $nombreGuardado.
// Comprueba si devuelve true. Guarda en $mensaje
// "Imagen subida" si va bien o un error si falla.
}
?>
<!doctype html>
<html lang="es"><head><meta charset="UTF-8"><title>Subida de imagen</title></head>
<body>
<h1>Subir imagen</h1>
<form method="post" action="" enctype="multipart/form-data">
<label for="imagen">Selecciona una imagen JPEG o PNG:</label>
<input id="imagen" name="imagen" type="file" accept="image/jpeg,image/png">
<button type="submit">Subir</button>
</form>
<?php // TODO 5: Muestra $mensaje, escapándolo.
// Si $nombreGuardado no está vacío Y la subida funcionó,
// muestra el nombre nuevo del archivo también escapado.
// No crees una URL pública de las imágenes en este ejercicio. ?>
</body></html>
Pruebas: sube JPEG, PNG, TXT, imagen mayor de 2 MB y formulario sin archivo. Comprueba los nombres nuevos en subidas. El atributo accept ayuda al navegador, pero la validación verdadera debe hacerse en PHP.
Criterios comunes de corrección para los diez ejercicios
- El archivo abre sin errores de sintaxis y la primera carga no produce avisos por índices inexistentes.
- El resultado esperado aparece únicamente cuando se envían datos adecuados.
- Los datos que llegan del formulario se validan y los textos mostrados en HTML se escapan.
- El alumno entrega el código completado y evidencia de al menos dos pruebas por ejercicio, incluyendo un error de entrada.

