Apuntes DAM
Volver al inicio

Ejercicios resueltos de Desarrollo Web en Entorno Servidor

Los 48 ejercicios de Desarrollo Web en Entorno Servidor de la web, tema a tema: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Arquitecturas y Tecnologías del Servidor

1. Analiza una petición HTTP

Medio · Arquitecturas y Tecnologías del Servidor · apuntesdam.com/subject/dwes/topic/arquitecturas-servidor

La entrada es una petición HTTP tal y como llega al servidor: la línea de petición, las cabeceras, una línea en blanco y, a veces, un cuerpo en formato de formulario (application/x-www-form-urlencoded). Muestra «Método: …», «Ruta: …» (sin la query string), una línea «GET nombre = valor» por cada parámetro de la URL, «Host: …», «Cabeceras: N» y una línea «POST nombre = valor» por cada campo del cuerpo. Los valores deben aparecer decodificados (%C3%B1 es ñ y + es un espacio).

Código de partida (php)
<?php$texto = stream_get_contents(STDIN);$lineas = explode("\n", str_replace("\r", '', $texto)); // TODO: línea de petición, cabeceras (hasta la primera línea vacía) y cuerpo

Ejemplo: Petición GET con parámetros

Entrada

GET /buscar?q=teclado+mec%C3%A1nico&pagina=2 HTTP/1.1
Host: tienda.es
Accept: text/html

Salida esperada

Método: GET
Ruta: /buscar
GET q = teclado mecánico
GET pagina = 2
Host: tienda.es
Cabeceras: 2

Ejemplo: Formulario con POST

Entrada

POST /registro HTTP/1.1
Host: academia.es
Content-Type: application/x-www-form-urlencoded
Content-Length: 45

nombre=Ana+Mu%C3%B1oz&email=ana%40correo.es&acepto=1

Salida esperada

Método: POST
Ruta: /registro
Host: academia.es
Cabeceras: 3
POST nombre = Ana Muñoz
POST email = ana@correo.es
POST acepto = 1

2. Construye la respuesta

Medio · Arquitecturas y Tecnologías del Servidor · apuntesdam.com/subject/dwes/topic/arquitecturas-servidor

Cada línea de la entrada describe una respuesta con «código;tipo;cuerpo». Escribe la respuesta HTTP completa: la línea de estado «HTTP/1.1 código texto» (200 OK, 201 Created, 204 No Content, 301 Moved Permanently, 404 Not Found, 500 Internal Server Error), «Content-Type: tipo; charset=utf-8», «Content-Length: N» con la longitud del cuerpo en bytes, una línea en blanco y el cuerpo. Una respuesta 204 no lleva cuerpo, ni Content-Type ni Content-Length. Si el código no está en la lista, escribe «Código no soportado: N». Separa cada respuesta de la siguiente con una línea «---».

Código de partida (php)
<?php$textos = [200 => 'OK']; // TODO: completa los códigos $respuestas = [];foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$codigo, $tipo, $cuerpo] = explode(';', $linea, 3);    // TODO: construye la respuesta y añádela a $respuestas}echo implode("\n---\n", $respuestas), "\n";

Ejemplo: Varias respuestas

Entrada

200;text/html;<h1>Hola</h1>
404;application/json;{"error":"No encontrado"}

Salida esperada

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 13

<h1>Hola</h1>
---
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 25

{"error":"No encontrado"}

Ejemplo: Bytes, 204 y códigos raros

Entrada

201;text/plain;Creado: añadido; ok
204;text/plain;
418;text/plain;Soy una tetera

Salida esperada

HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
Content-Length: 20

Creado: añadido; ok
---
HTTP/1.1 204 No Content
---
Código no soportado: 418

3. Un enrutador con parámetros

Difícil · Arquitecturas y Tecnologías del Servidor · apuntesdam.com/subject/dwes/topic/arquitecturas-servidor

Así deciden los frameworks qué código ejecutar para cada URL. La entrada empieza con rutas «MÉTODO patrón acción» (el patrón puede tener parámetros como /productos/{id}), una línea «---» y después peticiones «MÉTODO url». Para cada petición escribe la acción y sus parámetros en orden («verProducto id=42»), «404 No encontrado» si ninguna ruta encaja con la ruta de la URL, o «405 Método no permitido (GET, PUT)» si la ruta existe pero no para ese método (lista los métodos permitidos en el orden en que se definieron). Ignora la query string y una barra final (/productos/ equivale a /productos). Si varias rutas encajan, gana la primera definida.

Código de partida (php)
<?php$lineas = file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);$separador = array_search('---', $lineas);$rutas = array_slice($lineas, 0, $separador);$peticiones = array_slice($lineas, $separador + 1); foreach ($peticiones as $peticion) {    [$metodo, $url] = explode(' ', $peticion);    // TODO    echo "404 No encontrado\n";}

Ejemplo: Rutas básicas

Entrada

GET /productos listar
GET /productos/{id} verProducto
POST /productos crear
---
GET /productos
GET /productos/42
POST /productos
GET /clientes

Salida esperada

listar
verProducto id=42
crear
404 No encontrado

Ejemplo: Varios parámetros, 405 y barra final

Entrada

GET /usuarios/{usuario}/pedidos/{pedido} verPedido
GET /productos/{id} ver
PUT /productos/{id} actualizar
---
GET /usuarios/ana/pedidos/7?detalle=1
DELETE /productos/3
GET /productos/3/
PUT /productos/3

Salida esperada

verPedido usuario=ana pedido=7
405 Método no permitido (GET, PUT)
ver id=3
actualizar id=3

Fundamentos de PHP

4. Tabla de multiplicar en HTML

Fácil · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

PHP genera HTML. Lee un número de la entrada y escribe su tabla de multiplicar del 1 al 10 como una tabla HTML, con una fila por línea y dos espacios de sangría: « <tr><td>7 x 1</td><td>7</td></tr>». Si la entrada no es un entero entre 1 y 100, escribe «<p>Número no válido</p>».

Código de partida (php)
<?php$texto = trim((string) fgets(STDIN)); // TODO

Ejemplo: Tabla del 7

Entrada

7

Salida esperada

<table>
  <tr><td>7 x 1</td><td>7</td></tr>
  <tr><td>7 x 2</td><td>14</td></tr>
  <tr><td>7 x 3</td><td>21</td></tr>
  <tr><td>7 x 4</td><td>28</td></tr>
  <tr><td>7 x 5</td><td>35</td></tr>
  <tr><td>7 x 6</td><td>42</td></tr>
  <tr><td>7 x 7</td><td>49</td></tr>
  <tr><td>7 x 8</td><td>56</td></tr>
  <tr><td>7 x 9</td><td>63</td></tr>
  <tr><td>7 x 10</td><td>70</td></tr>
</table>

Ejemplo: Entrada no válida

Entrada

abc

Salida esperada

<p>Número no válido</p>

5. Encuentra el fallo: la frase que empieza por hola

Fácil · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

El programa dice qué frases contienen la palabra «hola». Con «hola mundo» responde que no la contiene, aunque es lo primero que aparece. Corrige la comprobación.

Código de partida (php)
<?php$frases = ["hola mundo", "dice hola", "adiós"];foreach ($frases as $f) {    if (strpos($f, "hola")) {        echo "$f: contiene hola\n";    } else {        echo "$f: no contiene hola\n";    }}

Ejemplo: Tres frases

Salida esperada

hola mundo: contiene hola
dice hola: contiene hola
adiós: no contiene hola

6. Encuentra el fallo: el saludo que suma

Fácil · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

El programa debería mostrar «Hola, Ana. Tienes 3 mensajes.», pero termina con un error. Corrígelo.

Código de partida (php)
<?php$nombre = "Ana";$mensajes = 3;echo "Hola, " + $nombre + ". Tienes " + $mensajes + " mensajes.\n";

Ejemplo: Saludo

Salida esperada

Hola, Ana. Tienes 3 mensajes.

7. Las palabras más repetidas

Medio · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

Lee un texto (puede tener varias líneas) y muestra las tres palabras más frecuentes con el formato «palabra: veces», en minúsculas. Una palabra es una secuencia de letras (incluidas las tildes y la ñ). Si dos palabras se repiten lo mismo, va primero la que sea menor alfabéticamente.

Código de partida (php)
<?php$texto = stream_get_contents(STDIN); // TODO

Ejemplo: Refrán

Entrada

El que mucho abarca poco aprieta, y el que poco abarca
mucho aprieta.

Salida esperada

abarca: 2
aprieta: 2
el: 2

Ejemplo: Mayúsculas y signos

Entrada

¡PHP! php, Php... Java; java. SQL

Salida esperada

php: 3
java: 2
sql: 1

8. Ticket de compra

Medio · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

Cada línea de la entrada es «producto;precio;cantidad». Escribe una línea por producto con el formato «Teclado x 2 = 49,98 €» y al final la base, el IVA y el total: «Base: 62,48 €», «IVA (21%): 13,12 €», «Total: 75,60 €». Las cantidades usan coma decimal y punto de miles (1.234,50 €). Las líneas que no tengan tres campos, un precio numérico y una cantidad entera se ignoran mostrando «Línea ignorada: …».

Código de partida (php)
<?php$lineas = file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // TODO

Ejemplo: Compra normal

Entrada

Teclado;24.99;2
Ratón;12.50;1

Salida esperada

Teclado x 2 = 49,98 €
Ratón x 1 = 12,50 €
Base: 62,48 €
IVA (21%): 13,12 €
Total: 75,60 €

Ejemplo: Líneas erróneas

Entrada

Monitor;189;1
Cable;barato;3
Funda;9.90
Portátil;999;1

Salida esperada

Monitor x 1 = 189,00 €
Línea ignorada: Cable;barato;3
Línea ignorada: Funda;9.90
Portátil x 1 = 999,00 €
Base: 1.188,00 €
IVA (21%): 249,48 €
Total: 1.437,48 €

9. Convertir y validar los datos de un formulario

Medio · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

Todo lo que llega de un formulario es texto, y convertirlo bien es la primera defensa. Cada línea de la entrada es «campo=valor». Valida y convierte según el campo: edad (entero de 0 a 120), email (dirección válida), nota (decimal de 0 a 10; admite coma o punto decimal) y web (URL que empiece por http o https). Para cada línea escribe «campo: valor convertido (tipo)» usando el tipo de PHP (int, float o string), o «campo: no válido». La nota se muestra con dos decimales. Los campos desconocidos se ignoran.

Código de partida (php)
<?phpforeach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$campo, $valor] = explode('=', $linea, 2) + [1 => ''];    $valor = trim($valor);     // TODO: convierte según el campo; false si no es válido    $resultado = false;     if ($resultado === false) {        echo "$campo: no válido\n";    } else {        echo "$campo: $resultado (" . get_debug_type($resultado) . ")\n";    }}

Ejemplo: Datos correctos

Entrada

edad=34
email=ana@correo.es
nota=7,5
web=https://apuntesdam.com

Salida esperada

edad: 34 (int)
email: ana@correo.es (string)
nota: 7.50 (float)
web: https://apuntesdam.com (string)

Ejemplo: Datos incorrectos

Entrada

edad=treinta
edad=150
email=ana@
nota=11
web=ftp://ficheros.es
color=rojo
nota=abc

Salida esperada

edad: no válido
edad: no válido
email: no válido
nota: no válido
web: no válido
nota: no válido

10. Encuentra el fallo: la subida de precios que no sube

Medio · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

El programa debería subir todos los precios un 10 % (redondeado a dos decimales) y listarlos, pero salen los precios de antes. Corrígelo.

Código de partida (php)
<?php$precios = ["pan" => 1.20, "leche" => 0.95, "café" => 4.50];foreach ($precios as $p) {    $p = round($p * 1.10, 2);}foreach ($precios as $producto => $p) {    echo "$producto: $p €\n";}

Ejemplo: Tres productos

Salida esperada

pan: 1.32 €
leche: 1.05 €
café: 4.95 €

11. Calendario del mes en HTML

Difícil · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

La entrada es un mes con el formato AAAA-MM. Genera su calendario como tabla HTML con la semana empezando en lunes: primero «<table>», una fila de cabecera « <tr><th>L</th><th>M</th><th>X</th><th>J</th><th>V</th><th>S</th><th>D</th></tr>», después una fila por semana con siete celdas (las celdas fuera del mes van vacías: «<td></td>»; los días, «<td>5</td>») y al final «</table>». Si la entrada no es un mes válido escribe «<p>Mes no válido</p>».

Código de partida (php)
<?php$texto = trim((string) fgets(STDIN)); // TODO

Ejemplo: Septiembre de 2026

Entrada

2026-09

Salida esperada

<table>
  <tr><th>L</th><th>M</th><th>X</th><th>J</th><th>V</th><th>S</th><th>D</th></tr>
  <tr><td></td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td></tr>
  <tr><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td><td>12</td><td>13</td></tr>
  <tr><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td><td>20</td></tr>
  <tr><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td><td>26</td><td>27</td></tr>
  <tr><td>28</td><td>29</td><td>30</td><td></td><td></td><td></td><td></td></tr>
</table>

Ejemplo: Mes no válido

Entrada

2026-13

Salida esperada

<p>Mes no válido</p>

12. Tu propio motor de plantillas

Muy difícil · Fundamentos de PHP · apuntesdam.com/subject/dwes/topic/php-fundamentos

Twig o Blade separan el HTML del código con un lenguaje de plantillas. Construye uno pequeño. La primera línea de la entrada son los datos en JSON y el resto, la plantilla. Soporta: {{ nombre }} y {{ usuario.email }} (valor escapado con htmlspecialchars; si no existe, cadena vacía), {{{ html }}} (sin escapar), {% for x in lista %}…{% endfor %} (sin anidar otros for) y {% if var %}…{% endif %} (se muestra si el valor es «verdadero» en PHP; puede ir dentro de un for). Escribe la plantilla ya procesada.

Código de partida (php)
<?php$datos = json_decode(fgets(STDIN), true);$plantilla = stream_get_contents(STDIN); function renderizar(string $tpl, array $ctx): string{    // TODO    return $tpl;} echo renderizar($plantilla, $datos);

Ejemplo: Variables y escape

Entrada

{"nombre":"Ana <3","usuario":{"email":"ana@correo.es"},"firma":"<b>Equipo</b>"}
<p>Hola, {{ nombre }} ({{usuario.email}})</p>
{{{ firma }}} {{ firma }} [{{ falta }}]

Salida esperada

<p>Hola, Ana &lt;3 (ana@correo.es)</p>
<b>Equipo</b> &lt;b&gt;Equipo&lt;/b&gt; []

Ejemplo: Bucles y condiciones

Entrada

{"titulo":"Pedidos","pedidos":[{"id":1,"urgente":true},{"id":2,"urgente":false},{"id":3,"urgente":1}],"vacio":[]}
<h1>{{ titulo }}</h1>
<ul>{% for p in pedidos %}<li>#{{ p.id }}{% if p.urgente %} ¡urgente!{% endif %}</li>{% endfor %}</ul>
{% if vacio %}Hay datos{% endif %}{% for x in vacio %}nunca{% endfor %}fin

Salida esperada

<h1>Pedidos</h1>
<ul><li>#1 ¡urgente!</li><li>#2</li><li>#3 ¡urgente!</li></ul>
fin

Formularios, Sesiones y Seguridad

13. Comentarios sin XSS

Fácil · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

Cada línea es un comentario «autor|texto» que han escrito los usuarios. El código ya los muestra como <li>, pero es vulnerable: un usuario podría colar HTML o JavaScript. Corrígelo para que los caracteres especiales se escapen (&lt; &gt; &amp; &quot; &#039;).

Código de partida (php)
<?phpforeach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    if (!str_contains($linea, '|')) {        continue;    }    [$autor, $texto] = explode('|', $linea, 2);    // TODO: escapa los datos antes de mostrarlos    echo "<li><strong>$autor</strong>: $texto</li>\n";}

Ejemplo: Comentarios normales

Entrada

Ana|Muy buen tema
Luis|Gracias

Salida esperada

<li><strong>Ana</strong>: Muy buen tema</li>
<li><strong>Luis</strong>: Gracias</li>

Ejemplo: Intento de ataque

Entrada

Hacker|<script>robar(document.cookie)</script>
Eva|Uso <b>negrita</b> & "comillas" y 'simples'

Salida esperada

<li><strong>Hacker</strong>: &lt;script&gt;robar(document.cookie)&lt;/script&gt;</li>
<li><strong>Eva</strong>: Uso &lt;b&gt;negrita&lt;/b&gt; &amp; &quot;comillas&quot; y &#039;simples&#039;</li>

14. Validar los datos de un formulario

Medio · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

La entrada es el cuerpo de un formulario enviado por POST (nombre=Ana&email=…&edad=…&web=…). Valida y escribe un error por línea, en este orden: «nombre: obligatorio» o «nombre: máximo 50 caracteres»; «email: no válido»; «edad: debe estar entre 18 y 120» (entero); «web: no válida» (es opcional, solo se valida si no está vacía). Si no hay errores, escribe «Datos correctos».

Código de partida (php)
<?phpparse_str(trim((string) fgets(STDIN)), $datos);$errores = []; // TODO

Ejemplo: Formulario correcto

Entrada

nombre=Ana+García&email=ana%40correo.es&edad=20&web=

Salida esperada

Datos correctos

Ejemplo: Todo mal

Entrada

nombre=++&email=ana&edad=17&web=hola

Salida esperada

nombre: obligatorio
email: no válido
edad: debe estar entre 18 y 120
web: no válida

Ejemplo: Nombre largo y edad decimal

Entrada

nombre=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&email=a%40b.es&edad=20.5&web=https%3A%2F%2Fapuntesdam.com

Salida esperada

nombre: máximo 50 caracteres
edad: debe estar entre 18 y 120

15. Un carrito guardado en la sesión

Medio · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

Simulamos $_SESSION con un array. Cada línea es una acción del usuario: «anadir id cantidad», «quitar id», «vaciar» o «ver». El catálogo tiene precios y stock. Al añadir, la cantidad debe ser un entero positivo y el total de ese producto en el carrito no puede superar el stock; si no se cumple, escribe «Error: …» («cantidad no válida», «producto inexistente» o «stock insuficiente») y no cambies nada. «ver» muestra una línea por producto en el orden en que se añadieron («Teclado x 2 = 49,98 €») y «Total: …», o «Carrito vacío».

Código de partida (php)
<?php$catalogo = [    't1' => ['nombre' => 'Teclado', 'precio' => 24.99, 'stock' => 5],    'r1' => ['nombre' => 'Ratón', 'precio' => 12.50, 'stock' => 2],    'm1' => ['nombre' => 'Monitor', 'precio' => 189.00, 'stock' => 1],];$_SESSION = ['carrito' => []];$euros = fn(float $n) => number_format($n, 2, ',', '.') . ' €'; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    $partes = explode(' ', trim($linea));    $accion = $partes[0];    // TODO}

Ejemplo: Añadir y ver

Entrada

anadir t1 2
anadir r1 1
anadir t1 1
ver

Salida esperada

Teclado x 3 = 74,97 €
Ratón x 1 = 12,50 €
Total: 87,47 €

Ejemplo: Errores y quitar

Entrada

anadir x9 1
anadir t1 0
anadir t1 dos
anadir r1 2
anadir r1 1
anadir m1 1
quitar r1
ver
vaciar
ver

Salida esperada

Error: producto inexistente
Error: cantidad no válida
Error: cantidad no válida
Error: stock insuficiente
Monitor x 1 = 189,00 €
Total: 189,00 €
Carrito vacío

16. Encuentra el fallo: el comentario que ejecuta código

Medio · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

La función pinta un comentario de un formulario. Si alguien escribe una etiqueta <script>, se mete en la página tal cual (XSS). Corrige la función para que el autor y el texto se muestren escapados.

Código de partida (php)
<?phpfunction tarjeta(string $autor, string $texto): string {    return "<p><b>$autor</b>: $texto</p>";} echo tarjeta("Ana", "¡Me encanta!") . "\n";echo tarjeta("Pirata", "<script>alert('hola')</script>") . "\n";

Ejemplo: Comentario normal y comentario con script

Salida esperada

<p><b>Ana</b>: ¡Me encanta!</p>
<p><b>Pirata</b>: &lt;script&gt;alert(&#039;hola&#039;)&lt;/script&gt;</p>

17. Inicio de sesión con bloqueo

Difícil · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

Las contraseñas se guardan cifradas con password_hash. Cada línea de la entrada es un intento «usuario contraseña» (la contraseña es lo que va después del primer espacio). Escribe «Bienvenido/a, usuario» si es correcto o «Usuario o contraseña incorrectos» si no (sin revelar si el usuario existe). Tras 3 fallos seguidos de un mismo usuario, su cuenta queda bloqueada: responde «Cuenta bloqueada» a cualquier intento posterior, aunque la contraseña sea correcta. Un acceso correcto reinicia sus fallos.

Código de partida (php)
<?php$usuarios = [    'ana' => password_hash('Secreta123', PASSWORD_DEFAULT),    'luis' => password_hash('DAW 2025!', PASSWORD_DEFAULT),];$fallos = []; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$usuario, $clave] = array_pad(explode(' ', $linea, 2), 2, '');    // TODO}

Ejemplo: Accesos correctos e incorrectos

Entrada

ana Secreta123
ana secreta123
luis DAW 2025!
pepe 1234

Salida esperada

Bienvenido/a, ana
Usuario o contraseña incorrectos
Bienvenido/a, luis
Usuario o contraseña incorrectos

Ejemplo: Bloqueo tras tres fallos

Entrada

ana a
ana b
ana c
ana Secreta123
luis DAW 2025!

Salida esperada

Usuario o contraseña incorrectos
Usuario o contraseña incorrectos
Usuario o contraseña incorrectos
Cuenta bloqueada
Bienvenido/a, luis

18. Cookies firmadas

Difícil · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

El usuario puede cambiar sus cookies, pero sí podemos detectar si lo ha hecho. Firmaremos cada valor con un HMAC: la cookie guarda «valor.firma», donde firma es hash_hmac('sha256', valor, clave secreta). Cada línea es «firmar texto» (escribe la cookie resultante) o «verificar cookie» (escribe «Válida: valor» o «Manipulada»). La clave secreta es la del código. Compara las firmas con hash_equals, que tarda lo mismo acierte o falle y no da pistas a un atacante. El valor puede contener puntos: la firma es lo que va tras el último punto.

Código de partida (php)
<?phpconst CLAVE = 'b7f1c0a9e2d34f5a8c6b1e0d9f2a4c7e'; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$orden, $dato] = explode(' ', $linea, 2) + [1 => ''];    // TODO}

Ejemplo: Firmar y verificar

Entrada

firmar tema=oscuro
firmar idioma=es
verificar tema=oscuro.4a4266b175ca852708e16e7683c741ffab3f889751364ef5d7fbcb1f91ee6922

Salida esperada

tema=oscuro.4a4266b175ca852708e16e7683c741ffab3f889751364ef5d7fbcb1f91ee6922
idioma=es.da4e78e48d02ba709bc82f09327775d9dd6e896a0c3c107193e13c25c9aba3a6
Válida: tema=oscuro

Ejemplo: Manipulaciones

Entrada

verificar tema=claro.4a4266b175ca852708e16e7683c741ffab3f889751364ef5d7fbcb1f91ee6922
verificar tema=oscuro.4a4266b175ca852708e16e7683c741ffab3f889751364ef5d7fbcb1f91ee6920
verificar sinpunto
verificar v1.2.c481f604fe4fafcded64054280f87bbb1002ae4cd73e5984b77b5de8275c2ac3

Salida esperada

Manipulada
Manipulada
Manipulada
Válida: v1.2

19. Control de acceso por roles y propiedad

Muy difícil · Formularios, Sesiones y Seguridad · apuntesdam.com/subject/dwes/topic/formularios-sesiones

La entrada tiene tres bloques separados por «---». Primero, los roles: «rol NOMBRE: permiso1, permiso2» o «rol NOMBRE hereda PADRE: permisos» (un rol tiene sus permisos y todos los de sus antepasados). Después, los usuarios: «NOMBRE: ROL». Por último, las peticiones: «USUARIO ACCION» o «USUARIO ACCION PROPIETARIO». Un usuario puede hacer ACCION si su rol tiene el permiso ACCION, o si tiene ACCION_propio y el recurso es suyo (PROPIETARIO es él). Escribe «USUARIO ACCION: permitido» o «… denegado». Un usuario desconocido siempre recibe denegado. Así se evitan los fallos de control de acceso, los primeros del OWASP Top 10.

Código de partida (php)
<?php$bloques = explode("---", stream_get_contents(STDIN));$lineas = fn(string $b) => array_filter(array_map('trim', explode("\n", $b))); // TODO: roles, usuarios y peticiones

Ejemplo: Herencia y propiedad

Entrada

rol lector: leer
rol autor hereda lector: crear, editar_propio, borrar_propio
rol editor hereda autor: editar, publicar
---
ana: autor
luis: editor
eva: lector
---
ana leer
ana editar luis
ana editar ana
ana publicar
luis editar ana
luis borrar ana
eva crear
pepe leer

Salida esperada

ana leer: permitido
ana editar: denegado
ana editar: permitido
ana publicar: denegado
luis editar: permitido
luis borrar: denegado
eva crear: denegado
pepe leer: denegado

Ejemplo: Varios niveles

Entrada

rol invitado: ver_catalogo
rol cliente hereda invitado: comprar, ver_pedido_propio, cancelar_pedido_propio
rol soporte hereda cliente: ver_pedido
rol admin hereda soporte: cancelar_pedido, gestionar_usuarios
---
marta: cliente
jorge: soporte
sara: admin
---
marta ver_catalogo
marta ver_pedido jorge
marta ver_pedido marta
jorge ver_pedido marta
jorge cancelar_pedido marta
jorge cancelar_pedido jorge
sara cancelar_pedido marta
sara gestionar_usuarios

Salida esperada

marta ver_catalogo: permitido
marta ver_pedido: denegado
marta ver_pedido: permitido
jorge ver_pedido: permitido
jorge cancelar_pedido: denegado
jorge cancelar_pedido: permitido
sara cancelar_pedido: permitido
sara gestionar_usuarios: permitido

POO y el Patrón MVC

20. Una entidad que se valida sola

Medio · POO y el Patrón MVC · apuntesdam.com/subject/dwes/topic/poo-mvc

Completa la clase Producto: el constructor usa promoción de propiedades (string $nombre, float $precio, int $stock = 0) y lanza InvalidArgumentException con «El nombre es obligatorio», «El precio no puede ser negativo» o «El stock no puede ser negativo». precioConIva() devuelve el precio con el 21 %. Al convertirlo en texto (__toString) debe dar «Teclado: 24,99 € (30,24 € con IVA) · stock 5». El programa ya lee líneas «nombre;precio;stock» y muestra el producto o «Error: mensaje».

Código de partida (php)
<?phpclass Producto{    // TODO: constructor con promoción de propiedades y validación, y precioConIva()     public function __toString(): string    {        return ''; // TODO    }} foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$nombre, $precio, $stock] = array_pad(explode(';', $linea), 3, '0');    try {        echo new Producto(trim($nombre), (float) $precio, (int) $stock), "\n";    } catch (InvalidArgumentException $e) {        echo 'Error: ', $e->getMessage(), "\n";    }}

Ejemplo: Productos válidos

Entrada

Teclado;24.99;5
Monitor;189;0

Salida esperada

Teclado: 24,99 € (30,24 € con IVA) · stock 5
Monitor: 189,00 € (228,69 € con IVA) · stock 0

Ejemplo: Errores de validación

Entrada

;10;1
Ratón;-3;2
Cable;2;-1

Salida esperada

Error: El nombre es obligatorio
Error: El precio no puede ser negativo
Error: El stock no puede ser negativo

21. Nóminas con herencia e interfaces

Medio · POO y el Patrón MVC · apuntesdam.com/subject/dwes/topic/poo-mvc

Completa la jerarquía. Empleado es abstracta: guarda el nombre y obliga a implementar salario(). Fijo cobra su sueldo base más 50 € por cada trienio (3 años completos de antigüedad); PorHoras cobra horas × precio, y las que pasen de 160 a precio × 1,5. Comercial extiende Fijo y además cobra un 5 % de sus ventas. Los que implementan la interfaz Bonificable (solo Comercial) suman bonus() a su salario en la nómina. Cada línea de la entrada es «tipo;nombre;datos…». Escribe «nombre: salario €» para cada uno, y al final «Total: …».

Código de partida (php)
<?phpinterface Bonificable{    public function bonus(): float;} abstract class Empleado{    public function __construct(public readonly string $nombre) {}     abstract public function salario(): float;} // TODO: completa Fijo, PorHoras y Comercial (que implementa Bonificable: bonus de 100 € si vende más de 20.000 €)class Fijo extends Empleado{    public function __construct(string $nombre, protected float $base, protected int $antiguedad)    {        parent::__construct($nombre);    }     public function salario(): float    {        return 0; // TODO    }} class PorHoras extends Empleado{    public function __construct(string $nombre, private float $horas, private float $precio)    {        parent::__construct($nombre);    }     public function salario(): float    {        return 0; // TODO    }} class Comercial extends Fijo // TODO: implements Bonificable{    public function __construct(string $nombre, float $base, int $antiguedad, private float $ventas)    {        parent::__construct($nombre, $base, $antiguedad);    }} $euros = fn(float $n) => number_format($n, 2, ',', '.') . ' €';$total = 0;foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    $d = explode(';', $linea);    $e = match ($d[0]) {        'fijo' => new Fijo($d[1], (float) $d[2], (int) $d[3]),        'horas' => new PorHoras($d[1], (float) $d[2], (float) $d[3]),        'comercial' => new Comercial($d[1], (float) $d[2], (int) $d[3], (float) $d[4]),    };    $importe = $e->salario() + ($e instanceof Bonificable ? $e->bonus() : 0);    $total += $importe;    echo "{$e->nombre}: {$euros($importe)}\n";}echo "Total: {$euros($total)}\n";

Ejemplo: Plantilla

Entrada

fijo;Ana;1800;7
horas;Luis;170;12
comercial;Eva;1200;2;25000

Salida esperada

Ana: 1.900,00 €
Luis: 2.100,00 €
Eva: 2.550,00 €
Total: 6.550,00 €

Ejemplo: Sin extras

Entrada

fijo;Marta;2000;2
horas;Pablo;100;15
comercial;Iker;1000;3;20000

Salida esperada

Marta: 2.000,00 €
Pablo: 1.500,00 €
Iker: 2.050,00 €
Total: 5.550,00 €

22. Encuentra el fallo: los intereses que no llegan

Medio · POO y el Patrón MVC · apuntesdam.com/subject/dwes/topic/poo-mvc

CuentaAhorro hereda de Cuenta y añade intereses, pero tras ingresar 1000 € y aplicar un 5 % el saldo sigue en 1000 € y aparecen avisos. Corrígelo cambiando lo mínimo.

Código de partida (php)
<?phpclass Cuenta {    private float $saldo = 0;    public function ingresar(float $c): void { $this->saldo += $c; }    public function getSaldo(): float { return $this->saldo; }} class CuentaAhorro extends Cuenta {    public function aplicarIntereses(float $tipo): void {        $this->saldo += $this->saldo * $tipo / 100;    }} $c = new CuentaAhorro();$c->ingresar(1000);$c->aplicarIntereses(5);echo "Saldo: " . $c->getSaldo() . " €\n";

Ejemplo: 1000 € al 5 %

Salida esperada

Saldo: 1050 €

23. Un motor de plantillas mínimo

Difícil · POO y el Patrón MVC · apuntesdam.com/subject/dwes/topic/poo-mvc

Separar la presentación de la lógica es la base de MVC. Completa render($plantilla, $datos): sustituye cada {{ clave }} por el valor de $datos escapado con htmlspecialchars, y {{ clave|raw }} por el valor sin escapar. Las claves pueden tener puntos para entrar en arrays anidados (usuario.nombre). Si una clave no existe, se sustituye por una cadena vacía. La primera línea de la entrada son los datos en JSON y el resto la plantilla.

Código de partida (php)
<?phpfunction render(string $plantilla, array $datos): string{    // TODO    return $plantilla;} $datos = json_decode((string) fgets(STDIN), true) ?? [];echo render(stream_get_contents(STDIN), $datos);

Ejemplo: Variables simples

Entrada

{"titulo": "Hola", "nombre": "Ana"}
<h1>{{ titulo }}</h1>
<p>Bienvenida, {{nombre}}</p>

Salida esperada

<h1>Hola</h1>
<p>Bienvenida, Ana</p>

Ejemplo: Escapado, raw y anidados

Entrada

{"usuario": {"nombre": "<Luis>", "rol": "admin"}, "aviso": "<b>Nuevo</b>"}
<p>{{ usuario.nombre }} ({{ usuario.rol }})</p>
{{ aviso|raw }} {{ aviso }}

Salida esperada

<p>&lt;Luis&gt; (admin)</p>
<b>Nuevo</b> &lt;b&gt;Nuevo&lt;/b&gt;

24. Una cadena de middlewares

Difícil · POO y el Patrón MVC · apuntesdam.com/subject/dwes/topic/poo-mvc

Los frameworks procesan cada petición con una cadena de middlewares: cada uno puede hacer algo antes, pasar la petición al siguiente con $siguiente($peticion) y hacer algo después, o cortar la cadena devolviendo su propia respuesta. Completa ejecutar(), que envuelve el controlador con los middlewares en orden (el primero es el más externo). Los middlewares ya están escritos: registro (anota la entrada y la salida), autenticación (401 si no hay usuario) y administración (403 si la ruta empieza por /admin y el rol no es admin). Cada línea de la entrada es «ruta usuario rol» (o «ruta - -» sin usuario).

Código de partida (php)
<?php$log = []; $registro = function (array $p, callable $siguiente) use (&$log) {    $log[] = "→ {$p['ruta']}";    $respuesta = $siguiente($p);    $log[] = "← {$respuesta[0]}";    return $respuesta;};$autenticacion = fn(array $p, callable $siguiente) => $p['usuario'] === null ? [401, 'Inicia sesión'] : $siguiente($p);$administracion = fn(array $p, callable $siguiente) =>    str_starts_with($p['ruta'], '/admin') && $p['rol'] !== 'admin' ? [403, 'Prohibido'] : $siguiente($p);$controlador = fn(array $p) => [200, "Hola, {$p['usuario']}: {$p['ruta']}"]; function ejecutar(array $middlewares, callable $controlador, array $peticion): array{    // TODO    return $controlador($peticion);} foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$ruta, $usuario, $rol] = explode(' ', $linea);    $peticion = ['ruta' => $ruta, 'usuario' => $usuario === '-' ? null : $usuario, 'rol' => $rol];    [$codigo, $cuerpo] = ejecutar([$registro, $autenticacion, $administracion], $controlador, $peticion);    echo "$codigo $cuerpo\n";}echo implode(' ', $log), "\n";

Ejemplo: Peticiones variadas

Entrada

/perfil ana usuario
/admin/usuarios ana usuario
/admin/usuarios luis admin
/perfil - -

Salida esperada

200 Hola, ana: /perfil
403 Prohibido
200 Hola, luis: /admin/usuarios
401 Inicia sesión
→ /perfil ← 200 → /admin/usuarios ← 403 → /admin/usuarios ← 200 → /perfil ← 401

Ejemplo: Solo anónimos

Entrada

/admin - -
/ - -

Salida esperada

401 Inicia sesión
401 Inicia sesión
→ /admin ← 401 → / ← 401

25. Un contenedor de dependencias con autowiring

Muy difícil · POO y el Patrón MVC · apuntesdam.com/subject/dwes/topic/poo-mvc

Laravel y Symfony crean los objetos por ti: miran el constructor de una clase, ven qué necesita y lo crean primero. Completa la clase Contenedor. get($clase) devuelve siempre la misma instancia de cada clase (singleton). Para crearla, usa ReflectionClass para leer los parámetros del constructor: si el tipo es una clase, la resuelve recursivamente con get; si es una interfaz, usa la implementación registrada con enlazar(); si es un tipo simple con valor por defecto, usa ese valor. Si detecta un ciclo (A necesita B y B necesita A) lanza LogicException «Dependencia circular: A → B → A». Si una interfaz no está enlazada, LogicException «Sin implementación para X». El programa ya muestra el orden en que se construyen los objetos.

Código de partida (php)
<?php$orden = []; interface Almacen {}final class AlmacenMemoria implements Almacen { public function __construct() { global $orden; $orden[] = 'AlmacenMemoria'; } }final class Reloj { public function __construct(public string $zona = 'Europe/Madrid') { global $orden; $orden[] = 'Reloj'; } }final class Registro { public function __construct(Reloj $reloj) { global $orden; $orden[] = 'Registro'; } }final class Repositorio { public function __construct(Almacen $almacen, Registro $registro) { global $orden; $orden[] = 'Repositorio'; } }final class Servicio { public function __construct(Repositorio $repo, Registro $registro, int $reintentos = 3) { global $orden; $orden[] = "Servicio($reintentos)"; } }final class Huevo { public function __construct(Gallina $g) {} }final class Gallina { public function __construct(Huevo $h) {} }interface Correo {}final class Avisos { public function __construct(Correo $c) {} } final class Contenedor{    private array $instancias = [];    private array $enlaces = [];     public function enlazar(string $interfaz, string $clase): void    {        $this->enlaces[$interfaz] = $clase;    }     public function get(string $clase): object    {        // TODO        return new $clase();    }} $c = new Contenedor();$c->enlazar(Almacen::class, AlmacenMemoria::class);foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $pedido) {    $orden = [];    try {        $a = $c->get($pedido);        echo "$pedido: " . (implode(', ', $orden) ?: 'ya existía') . ($a === $c->get($pedido) ? ' (misma instancia)' : '') . "\n";    } catch (Throwable $e) {        echo "$pedido: " . get_class($e) . ': ' . $e->getMessage() . "\n";    }}

Ejemplo: Árbol de dependencias

Entrada

Servicio
Repositorio
Almacen

Salida esperada

Servicio: AlmacenMemoria, Reloj, Registro, Repositorio, Servicio(3) (misma instancia)
Repositorio: ya existía (misma instancia)
Almacen: ya existía (misma instancia)

Ejemplo: Errores

Entrada

Huevo
Avisos
Reloj

Salida esperada

Huevo: LogicException: Dependencia circular: Huevo → Gallina → Huevo
Avisos: LogicException: Sin implementación para Correo
Reloj: Reloj (misma instancia)

Acceso a Datos con PDO

26. Encuentra el fallo: el listado de un solo producto

Fácil · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

La consulta devuelve tres productos ordenados por precio, pero el programa solo muestra el primero. Corrígelo para listarlos todos.

Código de partida (php)
<?php$pdo = new PDO("sqlite::memory:");$pdo->exec("CREATE TABLE productos (nombre TEXT, precio INTEGER)");$pdo->exec("INSERT INTO productos VALUES ('Teclado', 25), ('Ratón', 12), ('Monitor', 150)");$st = $pdo->query("SELECT nombre, precio FROM productos ORDER BY precio");if ($fila = $st->fetch(PDO::FETCH_ASSOC)) {    echo "{$fila['nombre']}: {$fila['precio']} €\n";}

Ejemplo: Tres productos

Salida esperada

Ratón: 12 €
Teclado: 25 €
Monitor: 150 €

27. Buscar sin inyección SQL

Medio · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

El programa lee una categoría y lista sus productos ordenados por precio («Teclado - 24,99 €»), o «Sin resultados». Funciona, pero construye la consulta pegando el texto del usuario: prueba a buscar ' OR '1'='1 y verás todos los productos. Reescríbelo con una consulta preparada.

Código de partida (php)
<?php$pdo = new PDO('sqlite::memory:', null, null, [    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,]);$pdo->exec("CREATE TABLE productos (id INTEGER PRIMARY KEY, nombre TEXT, categoria TEXT, precio REAL)");$pdo->exec("INSERT INTO productos (nombre, categoria, precio) VALUES    ('Teclado', 'perifericos', 24.99), ('Ratón', 'perifericos', 12.5),    ('Monitor 27', 'pantallas', 189), ('Portátil', 'equipos', 749),    ('Alfombrilla', 'perifericos', 6.9), ('Monitor 24', 'pantallas', 129)"); $categoria = trim((string) fgets(STDIN)); // TODO: esta consulta es vulnerable a inyección SQL$filas = $pdo->query("SELECT nombre, precio FROM productos WHERE categoria = '$categoria' ORDER BY precio")->fetchAll(); if (!$filas) {    echo "Sin resultados\n";}foreach ($filas as $f) {    echo $f['nombre'], ' - ', number_format($f['precio'], 2, ',', '.'), " €\n";}

Ejemplo: Búsqueda normal

Entrada

perifericos

Salida esperada

Alfombrilla - 6,90 €
Ratón - 12,50 €
Teclado - 24,99 €

Ejemplo: Intento de inyección

Entrada

' OR '1'='1

Salida esperada

Sin resultados

28. Paginar resultados con LIMIT

Medio · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

La tabla alumnos tiene 23 filas. La entrada es «página porPágina» (por ejemplo «2 5»). Muestra los nombres de esa página ordenados por id, uno por línea, y después «Página 2 de 5». Si la página o el tamaño no son enteros positivos, o la página no existe, muestra «Página fuera de rango».

Código de partida (php)
<?php$pdo = new PDO('sqlite::memory:', null, null, [    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,]);$pdo->exec("CREATE TABLE alumnos (id INTEGER PRIMARY KEY, nombre TEXT)");$insertar = $pdo->prepare('INSERT INTO alumnos (nombre) VALUES (?)');for ($i = 1; $i <= 23; $i++) {    $insertar->execute(["Alumno $i"]);} [$pagina, $tamano] = array_pad(preg_split('/\s+/', trim((string) fgets(STDIN))), 2, '');// TODO

Ejemplo: Segunda página

Entrada

2 5

Salida esperada

Alumno 6
Alumno 7
Alumno 8
Alumno 9
Alumno 10
Página 2 de 5

Ejemplo: Última página incompleta

Entrada

3 10

Salida esperada

Alumno 21
Alumno 22
Alumno 23
Página 3 de 3

Ejemplo: Fuera de rango

Entrada

6 5

Salida esperada

Página fuera de rango

29. Filtros dinámicos sin inyección

Medio · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

Cada línea de la entrada es un filtro opcional «clave=valor»: categoria, min (precio mínimo), max (precio máximo), texto (el nombre lo contiene) y orden (precio, precio_desc o nombre; cualquier otro valor se ignora y se ordena por nombre). Construye una única consulta preparada con solo los filtros recibidos y lista «nombre - precio €», o «Sin resultados». El texto se busca literalmente: los caracteres % y _ del usuario no deben funcionar como comodines de LIKE.

Código de partida (php)
<?php$pdo = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);$pdo->exec("CREATE TABLE productos (id INTEGER PRIMARY KEY, nombre TEXT, categoria TEXT, precio REAL)");$pdo->exec("INSERT INTO productos (nombre, categoria, precio) VALUES    ('Teclado mecánico', 'perifericos', 59.9), ('Ratón', 'perifericos', 12.5), ('Monitor 27', 'pantallas', 189),    ('Monitor 24', 'pantallas', 129), ('Cable USB_C', 'cables', 9.95), ('Cable USBxC', 'cables', 7.5),    ('Pack descuento 100%', 'ofertas', 99), ('Pack 1000', 'ofertas', 45), ('Alfombrilla', 'perifericos', 6.9)"); $filtros = [];foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$clave, $valor] = explode('=', $linea, 2) + [1 => ''];    $filtros[$clave] = $valor;} // TODO: construye la consulta con los filtros$filas = $pdo->query('SELECT nombre, precio FROM productos ORDER BY nombre')->fetchAll(); echo $filas ? '' : "Sin resultados\n";foreach ($filas as $f) {    echo $f['nombre'], ' - ', number_format($f['precio'], 2, ',', '.'), " €\n";}

Ejemplo: Categoría y precio

Entrada

categoria=perifericos
max=50
orden=precio_desc

Salida esperada

Ratón - 12,50 €
Alfombrilla - 6,90 €

Ejemplo: Comodines literales

Entrada

texto=USB_
orden=precio

Salida esperada

Cable USB_C - 9,95 €

Ejemplo: Inyección y orden raro

Entrada

categoria=' OR '1'='1
orden=precio; DROP TABLE productos

Salida esperada

Sin resultados

30. Transferencias con transacciones

Difícil · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

Cada línea es una transferencia «origen destino importe». Para cada una escribe «Cuenta inexistente», «Importe no válido» (menor o igual que 0 o no numérico), «Saldo insuficiente» o «Transferencia realizada», en ese orden de comprobación. Las dos actualizaciones de saldo deben hacerse dentro de una transacción. Al final, lista las cuentas ordenadas por id: «ES01 Ana: 400,00 €».

Código de partida (php)
<?php$pdo = new PDO('sqlite::memory:', null, null, [    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,]);$pdo->exec("CREATE TABLE cuentas (id TEXT PRIMARY KEY, titular TEXT, saldo REAL)");$pdo->exec("INSERT INTO cuentas VALUES ('ES01', 'Ana', 500), ('ES02', 'Luis', 120), ('ES03', 'Marta', 0)"); foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$origen, $destino, $importe] = array_pad(preg_split('/\s+/', trim($linea)), 3, '');    // TODO} foreach ($pdo->query('SELECT * FROM cuentas ORDER BY id') as $c) {    echo "{$c['id']} {$c['titular']}: ", number_format($c['saldo'], 2, ',', '.'), " €\n";}

Ejemplo: Varias operaciones

Entrada

ES01 ES02 100
ES02 ES03 300
ES03 ES01 0
ES09 ES01 10

Salida esperada

Transferencia realizada
Saldo insuficiente
Importe no válido
Cuenta inexistente
ES01 Ana: 400,00 €
ES02 Luis: 220,00 €
ES03 Marta: 0,00 €

Ejemplo: Encadenadas

Entrada

ES01 ES03 250.5
ES03 ES02 50.25
ES02 ES01 abc

Salida esperada

Transferencia realizada
Transferencia realizada
Importe no válido
ES01 Ana: 249,50 €
ES02 Luis: 170,25 €
ES03 Marta: 200,25 €

31. Un informe sin el problema N+1

Difícil · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

El programa muestra, para un año dado, cuántos pedidos ha hecho cada cliente y cuánto ha gastado, ordenado de mayor a menor gasto (a igual gasto, por nombre). Deben salir todos los clientes, también los que no han comprado ese año («0 pedidos, 0,00 €»). El código actual funciona, pero hace una consulta por cliente: con 10.000 clientes serían 10.001 consultas. Reescríbelo con una sola consulta (LEFT JOIN y GROUP BY). El programa cuenta las consultas ejecutadas y lo muestra al final.

Código de partida (php)
<?phpfinal class SentenciaContada extends PDOStatement{    public static int $ejecutadas = 0;    protected function __construct() {}    public function execute(?array $params = null): bool    {        self::$ejecutadas++;        return parent::execute($params);    }} $pdo = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);$pdo->exec("CREATE TABLE clientes (id INTEGER PRIMARY KEY, nombre TEXT)");$pdo->exec("CREATE TABLE pedidos (id INTEGER PRIMARY KEY, cliente_id INTEGER, fecha TEXT, total REAL)");$pdo->exec("INSERT INTO clientes (nombre) VALUES ('Ana'), ('Luis'), ('Marta'), ('Pablo'), ('Eva')");$pdo->exec("INSERT INTO pedidos (cliente_id, fecha, total) VALUES    (1, '2025-03-10', 120), (1, '2025-11-02', 80.5), (1, '2026-01-15', 45),    (2, '2025-06-01', 200.5), (3, '2026-02-20', 99.99), (3, '2026-05-05', 10),    (4, '2025-12-31', 0.01), (5, '2025-01-01', 200.5)");$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [SentenciaContada::class]); $anio = trim((string) fgets(STDIN));$euros = fn(float $n) => number_format($n, 2, ',', '.') . ' €'; // TODO: una sola consulta$clientes = $pdo->prepare('SELECT id, nombre FROM clientes');$clientes->execute();$informe = [];$porCliente = $pdo->prepare("SELECT COUNT(*) AS pedidos, COALESCE(SUM(total), 0) AS gasto FROM pedidos WHERE cliente_id = ? AND strftime('%Y', fecha) = ?");foreach ($clientes->fetchAll() as $c) {    $porCliente->execute([$c['id'], $anio]);    $informe[] = ['nombre' => $c['nombre']] + $porCliente->fetch();}usort($informe, fn($a, $b) => [$b['gasto'], $a['nombre']] <=> [$a['gasto'], $b['nombre']]); foreach ($informe as $fila) {    echo "{$fila['nombre']}: {$fila['pedidos']} pedidos, {$euros($fila['gasto'])}\n";}echo 'Consultas: ', SentenciaContada::$ejecutadas, "\n";

Ejemplo: Año 2025

Entrada

2025

Salida esperada

Ana: 2 pedidos, 200,50 €
Eva: 1 pedidos, 200,50 €
Luis: 1 pedidos, 200,50 €
Pablo: 1 pedidos, 0,01 €
Marta: 0 pedidos, 0,00 €
Consultas: 1

Ejemplo: Año 2026

Entrada

2026

Salida esperada

Marta: 2 pedidos, 109,99 €
Ana: 1 pedidos, 45,00 €
Eva: 0 pedidos, 0,00 €
Luis: 0 pedidos, 0,00 €
Pablo: 0 pedidos, 0,00 €
Consultas: 1

32. Importar un CSV: todo o nada

Muy difícil · Acceso a Datos con PDO · apuntesdam.com/subject/dwes/topic/acceso-datos-php

La entrada es un CSV con cabecera «sku,nombre,precio,stock». Procesa las filas en orden dentro de una transacción: si el SKU ya existe en la tabla, actualiza nombre, precio y stock; si no, lo inserta. Valida cada fila antes de guardarla: 4 campos («número de campos incorrecto»), SKU con el formato AAA-000 («SKU no válido»), nombre no vacío («nombre vacío»), precio numérico ≥ 0 («precio no válido»), stock entero ≥ 0 («stock no válido») y que el SKU no se repita en el fichero («SKU repetido»). En cuanto una fila falle, escribe «Línea N: motivo» (la cabecera es la línea 1), deshaz la transacción y escribe «Importación cancelada». Si todo va bien, confirma y escribe «Nuevos: X, actualizados: Y». En ambos casos termina listando la tabla ordenada por SKU («TEC-001 Teclado 24.99 5»).

Código de partida (php)
<?php$pdo = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);$pdo->exec("CREATE TABLE productos (sku TEXT PRIMARY KEY, nombre TEXT NOT NULL, precio REAL NOT NULL, stock INTEGER NOT NULL)");$pdo->exec("INSERT INTO productos VALUES ('TEC-001', 'Teclado', 24.99, 5), ('RAT-001', 'Ratón', 12.5, 10)"); $lineas = file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);array_shift($lineas); // cabecera // TODO: validar e importar dentro de una transacción foreach ($pdo->query('SELECT * FROM productos ORDER BY sku') as $p) {    echo "{$p['sku']} {$p['nombre']} {$p['precio']} {$p['stock']}\n";}

Ejemplo: Importación correcta

Entrada

sku,nombre,precio,stock
MON-027,"Monitor 27""",189,3
TEC-001,Teclado mecánico,59.9,8
CAB-010,"Cable, 2 m",4.5,100

Salida esperada

Nuevos: 2, actualizados: 1
CAB-010 Cable, 2 m 4.5 100
MON-027 Monitor 27" 189 3
RAT-001 Ratón 12.5 10
TEC-001 Teclado mecánico 59.9 8

Ejemplo: Un error deshace todo

Entrada

sku,nombre,precio,stock
MON-027,Monitor,189,3
TEC-001,Teclado nuevo,30,2
ALF-01,Alfombrilla,6.9,1

Salida esperada

Línea 4: SKU no válido
Importación cancelada
RAT-001 Ratón 12.5 10
TEC-001 Teclado 24.99 5

Servicios Web y APIs REST

33. Resumir la respuesta de una API

Medio · Servicios Web y APIs REST · apuntesdam.com/subject/dwes/topic/servicios-web

La entrada es la respuesta JSON de una API de repositorios: un array de objetos con name, language (puede ser null) y stars. Muestra los tres con más estrellas («1. nombre (★ 1.234)», con punto de miles), después «Total de estrellas: N» y «Lenguajes: …» con los lenguajes distintos ordenados alfabéticamente y separados por comas, sin contar null. Si el JSON no es válido, muestra «JSON no válido».

Código de partida (php)
<?php$texto = stream_get_contents(STDIN); // TODO

Ejemplo: Repositorios

Entrada

[{"name": "apuntes", "language": "TypeScript", "stars": 320}, {"name": "tienda-php", "language": "PHP", "stars": 1450}, {"name": "dotfiles", "language": null, "stars": 12}, {"name": "api-rest", "language": "PHP", "stars": 870}, {"name": "juego", "language": "JavaScript", "stars": 95}]

Salida esperada

1. tienda-php (★ 1.450)
2. api-rest (★ 870)
3. apuntes (★ 320)
Total de estrellas: 2.747
Lenguajes: JavaScript, PHP, TypeScript

Ejemplo: JSON roto

Entrada

[{"name": "x", 

Salida esperada

JSON no válido

34. Caché HTTP con ETag

Medio · Servicios Web y APIs REST · apuntesdam.com/subject/dwes/topic/servicios-web

Para no enviar una y otra vez los mismos datos, una API puede responder con una ETag (una huella del contenido). El cliente la guarda y, en la siguiente petición, la envía en la cabecera If-None-Match; si el recurso no ha cambiado, el servidor responde 304 sin cuerpo. Cada línea de la entrada es «id etag-recibida» (o «id -» si el cliente no envía ninguna). Para cada una: si el producto no existe, «404»; si no, calcula la ETag como "…" con el md5 del JSON del producto y escribe «304» si coincide con la recibida, o «200 ETag: "…" {json}» si no.

Código de partida (php)
<?php$productos = [    7 => ['id' => 7, 'nombre' => 'Teclado', 'precio' => 24.99],    8 => ['id' => 8, 'nombre' => 'Ratón', 'precio' => 12.5],]; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$id, $recibida] = explode(' ', $linea, 2);    // TODO: 404, 304 o 200 con ETag    echo '200 ', json_encode($productos[(int) $id] ?? null, JSON_UNESCAPED_UNICODE), "\n";}

Ejemplo: Primera petición y repetición

Entrada

7 -
7 "20d295fd089a83559e889d2e4b093c30"

Salida esperada

200 ETag: "20d295fd089a83559e889d2e4b093c30" {"id":7,"nombre":"Teclado","precio":24.99}
304

Ejemplo: ETag antigua y producto inexistente

Entrada

8 "20d295fd089a83559e889d2e4b093c30"
9 -
8 "8ce973fc634b7edd437fd1757591f55f"

Salida esperada

200 ETag: "8ce973fc634b7edd437fd1757591f55f" {"id":8,"nombre":"Ratón","precio":12.5}
404
304

35. Una API REST de libros

Difícil · Servicios Web y APIs REST · apuntesdam.com/subject/dwes/topic/servicios-web

Cada línea de la entrada es una petición «MÉTODO RUTA [cuerpo JSON]». Escribe para cada una el código de estado y, si hay cuerpo, un espacio y la respuesta en JSON (json_encode con JSON_UNESCAPED_UNICODE). GET /api/libros → 200 con la lista; GET /api/libros/{id} → 200 con el libro o 404 {"error":"Libro no encontrado"}; POST /api/libros con titulo y autor → 201 con el libro creado (id = mayor id + 1) o 422 {"error":"Faltan campos"}; DELETE /api/libros/{id} → 204 sin cuerpo o 404. Cualquier otro método sobre esas rutas → 405 {"error":"Método no permitido"} y cualquier otra ruta → 404 {"error":"Ruta no encontrada"}.

Código de partida (php)
<?php$libros = [    1 => ['id' => 1, 'titulo' => 'El Quijote', 'autor' => 'Cervantes'],    2 => ['id' => 2, 'titulo' => 'La Regenta', 'autor' => 'Clarín'],]; function responder(int $codigo, mixed $datos = null): void{    echo $codigo, $datos === null ? '' : ' ' . json_encode($datos, JSON_UNESCAPED_UNICODE), "\n";} foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$metodo, $ruta, $cuerpo] = array_pad(explode(' ', $linea, 3), 3, '');    // TODO}

Ejemplo: Consultar

Entrada

GET /api/libros
GET /api/libros/2
GET /api/libros/9

Salida esperada

200 [{"id":1,"titulo":"El Quijote","autor":"Cervantes"},{"id":2,"titulo":"La Regenta","autor":"Clarín"}]
200 {"id":2,"titulo":"La Regenta","autor":"Clarín"}
404 {"error":"Libro no encontrado"}

Ejemplo: Crear y borrar

Entrada

POST /api/libros {"titulo": "Niebla", "autor": "Unamuno"}
POST /api/libros {"titulo": "Sin autor"}
DELETE /api/libros/1
GET /api/libros

Salida esperada

201 {"id":3,"titulo":"Niebla","autor":"Unamuno"}
422 {"error":"Faltan campos"}
204
200 [{"id":2,"titulo":"La Regenta","autor":"Clarín"},{"id":3,"titulo":"Niebla","autor":"Unamuno"}]

Ejemplo: Errores de método y ruta

Entrada

PUT /api/libros/2
PATCH /api/libros
GET /api/autores

Salida esperada

405 {"error":"Método no permitido"}
405 {"error":"Método no permitido"}
404 {"error":"Ruta no encontrada"}

36. Tokens JWT firmados a mano

Difícil · Servicios Web y APIs REST · apuntesdam.com/subject/dwes/topic/servicios-web

Un JWT tiene tres partes en base64url separadas por puntos: cabecera, datos (payload) y firma HMAC-SHA256 de «cabecera.payload» con una clave secreta. Completa crear(array $datos): string y verificar(string $token, int $ahora): ?array. verificar devuelve los datos si el token tiene tres partes, la firma coincide (hash_equals) y no ha caducado (exp mayor que $ahora); si no, null. La entrada tiene órdenes «crear usuario rol exp» (escribe el token) y «verificar token ahora» (escribe «usuario (rol)» o «Token no válido»).

Código de partida (php)
<?phpconst CLAVE = 'clave-super-secreta-de-32-bytes!'; function b64url(string $s): string{    return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');} function crear(array $datos): string{    // TODO    return '';} function verificar(string $token, int $ahora): ?array{    // TODO    return null;} foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    $p = explode(' ', $linea);    if ($p[0] === 'crear') {        echo crear(['sub' => $p[1], 'rol' => $p[2], 'exp' => (int) $p[3]]), "\n";    } else {        $d = verificar($p[1], (int) $p[2]);        echo $d ? "{$d['sub']} ({$d['rol']})\n" : "Token no válido\n";    }}

Ejemplo: Crear y verificar

Entrada

crear ana admin 2000
verificar eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmEiLCJyb2wiOiJhZG1pbiIsImV4cCI6MjAwMH0.IaJuoy2wd_oP6WTgrfU1Ny2SyxlpU5xvgqWO_DHmGoM 1000

Salida esperada

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmEiLCJyb2wiOiJhZG1pbiIsImV4cCI6MjAwMH0.IaJuoy2wd_oP6WTgrfU1Ny2SyxlpU5xvgqWO_DHmGoM
ana (admin)

Ejemplo: Caducado y manipulado

Entrada

verificar eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsdWlzIiwicm9sIjoiY2xpZW50ZSIsImV4cCI6MTUwMH0.Wm4XR0ADR0f3fufjlvLLdVgmurqmGPjsM7kRNYPZ3Q4 1500
verificar eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsdWlzIiwicm9sIjoiY2xpZW50ZSIsImV4cCI6MTUwMH0.Wm4XR0ADR0f3fufjlvLLdVgmurqmGPjsM7kRNYPZ3Q4 1499
verificar eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmEiLCJyb2wiOiJhZG1pbiIsImV4cCI6OTk5OX0.IaJuoy2wd_oP6WTgrfU1Ny2SyxlpU5xvgqWO_DHmGoM 1000
verificar basura 1000

Salida esperada

Token no válido
luis (cliente)
Token no válido
Token no válido

37. Limitar las peticiones: 429 Too Many Requests

Muy difícil · Servicios Web y APIs REST · apuntesdam.com/subject/dwes/topic/servicios-web

Toda API pública limita cuántas peticiones acepta de cada cliente. Implementa una ventana deslizante: un cliente puede hacer como máximo 3 peticiones en cualquier intervalo de 10 segundos. Cada línea de la entrada es «segundo cliente» (los segundos no bajan). Si la petición se admite, escribe «segundo cliente: 200 (quedan N)»; si no, «segundo cliente: 429 Retry-After: S», donde S es cuántos segundos faltan para que la petición más antigua de su ventana salga de ella. Las peticiones rechazadas no cuentan. Al final, escribe cuántas peticiones se admitieron por cliente, en orden alfabético.

Código de partida (php)
<?phpconst LIMITE = 3;const VENTANA = 10;$historial = [];$admitidas = []; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$ahora, $cliente] = explode(' ', $linea);    $ahora = (int) $ahora;    // TODO    echo "$ahora $cliente: 200 (quedan 2)\n";} ksort($admitidas);foreach ($admitidas as $cliente => $n) {    echo "$cliente: $n admitidas\n";}

Ejemplo: Un cliente insistente

Entrada

0 ana
1 ana
2 ana
3 ana
9 ana
10 ana
11 ana

Salida esperada

0 ana: 200 (quedan 2)
1 ana: 200 (quedan 1)
2 ana: 200 (quedan 0)
3 ana: 429 Retry-After: 7
9 ana: 429 Retry-After: 1
10 ana: 200 (quedan 0)
11 ana: 200 (quedan 0)
ana: 5 admitidas

Ejemplo: Clientes independientes

Entrada

0 luis
0 eva
1 luis
2 luis
2 eva
5 luis
12 luis

Salida esperada

0 luis: 200 (quedan 2)
0 eva: 200 (quedan 2)
1 luis: 200 (quedan 1)
2 luis: 200 (quedan 0)
2 eva: 200 (quedan 1)
5 luis: 429 Retry-After: 5
12 luis: 200 (quedan 2)
eva: 2 admitidas
luis: 4 admitidas

Aplicaciones Dinámicas e Híbridas

38. El endpoint de un autocompletado

Medio · Aplicaciones Dinámicas e Híbridas · apuntesdam.com/subject/dwes/topic/aplicaciones-hibridas

Un campo de búsqueda de municipios pide sugerencias al servidor mientras el usuario escribe. Cada línea de la entrada es la query string que llega (q=…). Responde con un array JSON (una línea por petición, sin escapar las tildes) de como mucho 5 objetos {"nombre", "provincia"}: primero los municipios que empiezan por el texto y después los que lo contienen en otra posición, cada grupo en orden alfabético. La búsqueda ignora mayúsculas, tildes y espacios a los lados. Con menos de 2 caracteres, responde [].

Código de partida (php)
<?php$municipios = [    ['Arnedo', 'La Rioja'], ['Logroño', 'La Rioja'], ['Calahorra', 'La Rioja'], ['Haro', 'La Rioja'],    ['Alfaro', 'La Rioja'], ['Ávila', 'Ávila'], ['Almería', 'Almería'], ['Alicante', 'Alicante'],    ['Albacete', 'Albacete'], ['Alcalá de Henares', 'Madrid'], ['Madrid', 'Madrid'], ['Málaga', 'Málaga'],    ['Salamanca', 'Salamanca'], ['Cádiz', 'Cádiz'], ['Jaén', 'Jaén'], ['León', 'León'], ['Lleida', 'Lleida'],    ['Logrosán', 'Cáceres'], ['Santander', 'Cantabria'], ['Zaragoza', 'Zaragoza'], ['Alcobendas', 'Madrid'],    ['Aranjuez', 'Madrid'],]; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    parse_str($linea, $q);    $texto = trim($q['q'] ?? '');    // TODO    $sugerencias = [];    echo json_encode($sugerencias, JSON_UNESCAPED_UNICODE), "\n";}

Ejemplo: Prefijos y límite de 5

Entrada

q=al

Salida esperada

[{"nombre":"Albacete","provincia":"Albacete"},{"nombre":"Alcalá de Henares","provincia":"Madrid"},{"nombre":"Alcobendas","provincia":"Madrid"},{"nombre":"Alfaro","provincia":"La Rioja"},{"nombre":"Alicante","provincia":"Alicante"}]

Ejemplo: Mayúsculas, tildes y mínimo

Entrada

q=LOGRO
q=%C3%A1vila
q=a

Salida esperada

[{"nombre":"Logroño","provincia":"La Rioja"},{"nombre":"Logrosán","provincia":"Cáceres"}]
[{"nombre":"Ávila","provincia":"Ávila"}]
[]

39. Verificar la firma de un webhook

Difícil · Aplicaciones Dinámicas e Híbridas · apuntesdam.com/subject/dwes/topic/aplicaciones-hibridas

Cuando un pago se completa, la pasarela avisa a tu servidor con una petición (un webhook). Cualquiera podría enviar una petición falsa a esa URL, así que la pasarela la firma: la cabecera Firma es «t=segundos,v1=hmac», donde hmac es hash_hmac('sha256', "$t.$cuerpo", secreto). Puede haber varios v1 (mientras se cambia de secreto) y basta con que coincida uno. Cada línea de la entrada es «ahora|cabecera|cuerpo». Responde «Aceptado: tipo» (el campo tipo del JSON del cuerpo), «Rechazado: cabecera» si falta t o no hay ningún v1, «Rechazado: caducado» si la firma tiene más de 300 segundos (evita que alguien reenvíe un aviso antiguo) o «Rechazado: firma».

Código de partida (php)
<?phpconst SECRETO = 'whsec_9f86d081884c7d659a2feaa0c55ad015'; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$ahora, $cabecera, $cuerpo] = explode('|', $linea, 3);    // TODO    echo 'Aceptado: ', json_decode($cuerpo, true)['tipo'] ?? '?', "\n";}

Ejemplo: Firma correcta y secretos en rotación

Entrada

1790000010|t=1790000000,v1=a66b90c2ce77ad964f837ff815c1bfb648250c7ae018143fc7deaf5bd70adc04|{"tipo":"pago.completado","pedido":42}
1790000000|t=1790000000,v1=6d8e2dca4474029af715b92b92446f8ccfb7482dbef34e7e74d6b66bdaab76e9,v1=3b8cfd66d6a6b47bbca67eecba09df7144505aaa108217791e8254bd1cff55cd|{"tipo":"pago.fallido","pedido":43}

Salida esperada

Aceptado: pago.completado
Aceptado: pago.fallido

Ejemplo: Ataques

Entrada

1790000000|t=1790000000,v1=a66b90c2ce77ad964f837ff815c1bfb648250c7ae018143fc7deaf5bd70adc04|{"tipo":"pago.completado","pedido":99}
1790000301|t=1790000000,v1=a66b90c2ce77ad964f837ff815c1bfb648250c7ae018143fc7deaf5bd70adc04|{"tipo":"pago.completado","pedido":42}
1790000000|v1=a66b90c2ce77ad964f837ff815c1bfb648250c7ae018143fc7deaf5bd70adc04|{"tipo":"pago.completado","pedido":42}
1790000000|t=1790000000|{"tipo":"pago.completado","pedido":42}

Salida esperada

Rechazado: firma
Rechazado: caducado
Rechazado: cabecera
Rechazado: cabecera

40. Un cortacircuitos para un servicio externo

Muy difícil · Aplicaciones Dinámicas e Híbridas · apuntesdam.com/subject/dwes/topic/aplicaciones-hibridas

Si la API de envíos se cae, cada petición a tu tienda esperaría el tiempo máximo antes de fallar, y la tienda entera se volvería lenta. El patrón cortacircuitos (circuit breaker) lo evita. Empieza cerrado (las llamadas pasan). Tras 3 fallos seguidos se abre: durante 30 segundos las peticiones se rechazan al momento sin llamar. Pasados 30 segundos, la siguiente petición es una prueba: si sale bien se cierra (y se reinicia la cuenta de fallos); si falla, vuelve a abrirse otros 30 segundos. Un éxito con el circuito cerrado también reinicia la cuenta. Cada línea es «segundo resultado», donde resultado es lo que respondería el servicio si se le llamara (ok o fallo). Escribe, según el caso: «t: ok (cerrado)», «t: fallo (cerrado, N fallos)», «t: fallo → abierto», «t: rechazada (abierto)», «t: prueba ok → cerrado» o «t: prueba fallo → abierto». Al final, «Llamadas reales: N».

Código de partida (php)
<?phpconst UMBRAL = 3;const ESPERA = 30;$estado = 'cerrado';$fallos = 0;$abiertoDesde = 0;$llamadas = 0; foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$t, $resultado] = explode(' ', $linea);    $t = (int) $t;    // TODO    $llamadas++;    echo "$t: $resultado (cerrado)\n";}echo "Llamadas reales: $llamadas\n";

Ejemplo: Se abre y la prueba falla

Entrada

0 ok
1 fallo
2 fallo
3 fallo
10 ok
32 ok
33 fallo

Salida esperada

0: ok (cerrado)
1: fallo (cerrado, 1 fallos)
2: fallo (cerrado, 2 fallos)
3: fallo → abierto
10: rechazada (abierto)
32: rechazada (abierto)
33: prueba fallo → abierto
Llamadas reales: 5

Ejemplo: Un éxito reinicia y la prueba sale bien

Entrada

0 fallo
1 ok
2 fallo
3 fallo
4 fallo
40 ok
41 fallo

Salida esperada

0: fallo (cerrado, 1 fallos)
1: ok (cerrado)
2: fallo (cerrado, 1 fallos)
3: fallo (cerrado, 2 fallos)
4: fallo → abierto
40: prueba ok → cerrado
41: fallo (cerrado, 1 fallos)
Llamadas reales: 7

Ejercicios de Desarrollo Web en Entorno Servidor

41. Informe a partir de un CSV

Medio · Ejercicios de Desarrollo Web en Entorno Servidor · apuntesdam.com/subject/dwes/topic/ejercicios-dwes

La entrada es un CSV con cabecera «fecha,comercial,importe» (los campos pueden ir entre comillas). Suma el importe por comercial y muestra una línea por comercial ordenada de mayor a menor importe («Ana: 3.200,50 €»), después «Total: …» y, si alguna fila no tenía 3 campos o un importe numérico, «Filas descartadas: N».

Código de partida (php)
<?php$lineas = file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // TODO

Ejemplo: Ventas

Entrada

fecha,comercial,importe
2025-01-10,Ana,1200.50
2025-01-11,Luis,800
2025-01-15,Ana,2000
2025-02-01,"Marta",950

Salida esperada

Ana: 3.200,50 €
Marta: 950,00 €
Luis: 800,00 €
Total: 4.950,50 €

Ejemplo: Filas erróneas

Entrada

fecha,comercial,importe
2025-03-01,Luis,100
2025-03-02,Ana
2025-03-03,Marta,mucho
2025-03-04,"Pérez, Juan",4500

Salida esperada

Pérez, Juan: 4.500,00 €
Luis: 100,00 €
Total: 4.600,00 €
Filas descartadas: 2

42. API de tareas con base de datos

Difícil · Ejercicios de Desarrollo Web en Entorno Servidor · apuntesdam.com/subject/dwes/topic/ejercicios-dwes

Une una API REST con PDO. La tabla tareas (id, titulo, hecha) empieza vacía. Cada línea es «MÉTODO RUTA [JSON]» y la respuesta, «código JSON». POST /tareas {"titulo": …} → 201 con la tarea ({"id":1,"titulo":…,"hecha":false}) o 422 {"error":"El título es obligatorio"}; GET /tareas → 200 con todas por id; GET /tareas?hecha=1 o ?hecha=0 → filtradas; PATCH /tareas/{id} {"hecha": true} → 200 con la tarea actualizada; DELETE /tareas/{id} → 204 sin cuerpo. Un id inexistente → 404 {"error":"Tarea no encontrada"}. Todas las consultas, preparadas; hecha se guarda como 0/1 y se devuelve como booleano.

Código de partida (php)
<?php$pdo = new PDO('sqlite::memory:', null, null, [    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,]);$pdo->exec('CREATE TABLE tareas (id INTEGER PRIMARY KEY, titulo TEXT NOT NULL, hecha INTEGER NOT NULL DEFAULT 0)'); function responder(int $codigo, mixed $datos = null): void{    echo $codigo, $datos === null ? '' : ' ' . json_encode($datos, JSON_UNESCAPED_UNICODE), "\n";} foreach (file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {    [$metodo, $ruta, $cuerpo] = array_pad(explode(' ', $linea, 3), 3, '');    // TODO}

Ejemplo: Crear y listar

Entrada

POST /tareas {"titulo": "Estudiar PDO"}
POST /tareas {"titulo": "Hacer la API"}
POST /tareas {"titulo": "  "}
GET /tareas

Salida esperada

201 {"id":1,"titulo":"Estudiar PDO","hecha":false}
201 {"id":2,"titulo":"Hacer la API","hecha":false}
422 {"error":"El título es obligatorio"}
200 [{"id":1,"titulo":"Estudiar PDO","hecha":false},{"id":2,"titulo":"Hacer la API","hecha":false}]

Ejemplo: Actualizar, filtrar y borrar

Entrada

POST /tareas {"titulo": "A"}
POST /tareas {"titulo": "B"}
PATCH /tareas/2 {"hecha": true}
GET /tareas?hecha=1
GET /tareas?hecha=0
DELETE /tareas/1
DELETE /tareas/1
GET /tareas

Salida esperada

201 {"id":1,"titulo":"A","hecha":false}
201 {"id":2,"titulo":"B","hecha":false}
200 {"id":2,"titulo":"B","hecha":true}
200 [{"id":2,"titulo":"B","hecha":true}]
200 [{"id":1,"titulo":"A","hecha":false}]
204
404 {"error":"Tarea no encontrada"}
200 [{"id":2,"titulo":"B","hecha":true}]

43. Carrito con cupones

Difícil · Ejercicios de Desarrollo Web en Entorno Servidor · apuntesdam.com/subject/dwes/topic/ejercicios-dwes

La entrada tiene líneas «producto;precio;cantidad» y, opcionalmente, líneas «CUPON código». El envío cuesta 4,95 € si el subtotal es menor de 50 €. Cupones: DESC10 (10 % del subtotal), MENOS5 (5 € si el subtotal es de al menos 30 €) y ENVIOGRATIS (sin gastos de envío). Solo se aplica un cupón: el que más ahorre de los introducidos (los códigos desconocidos o que no cumplan su condición se ignoran). Muestra «Subtotal: …», «Descuento: …» (con el código entre paréntesis si hay cupón, «Descuento (DESC10): 5,00 €»; si no, «Descuento: 0,00 €»), «Envío: …» y «Total: …».

Código de partida (php)
<?php$lineas = file('php://stdin', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // TODO

Ejemplo: Sin cupón

Entrada

Libro;19.90;1
Bolígrafo;1.20;5

Salida esperada

Subtotal: 25,90 €
Descuento: 0,00 €
Envío: 4,95 €
Total: 30,85 €

Ejemplo: Elige el mejor cupón

Entrada

Auriculares;39.90;1
CUPON MENOS5
CUPON ENVIOGRATIS
CUPON DESC10

Salida esperada

Subtotal: 39,90 €
Descuento (MENOS5): 5,00 €
Envío: 4,95 €
Total: 39,85 €

Ejemplo: Cupón que no cumple y otro que sí

Entrada

Funda;12;2
CUPON MENOS5
CUPON FALSO
CUPON DESC10

Salida esperada

Subtotal: 24,00 €
Descuento (DESC10): 2,40 €
Envío: 4,95 €
Total: 26,55 €

Ejercicios largos

44. Factura de un carrito con IVA y promociones

Difícil · PHP · 70 minutos · apuntesdam.com/ejercicios/php/factura-con-promociones

Una tienda online pequeña guarda el carrito de cada cliente como texto, con una línea por producto: nombre, categoría, precio sin IVA y unidades. Antes de cobrar, necesita un programa que convierta ese carrito en una factura clara, con las promociones aplicadas y el IVA desglosado.

Para simplificar, la tienda agrupa sus productos en tres categorías y cada una tiene un único tipo de IVA (en la realidad hay alimentos al 4 % y otros al 10 %, pero aquí no hace falta afinar tanto). Además tiene dos promociones: un 3x2 en alimentación y un 5 % de descuento en los productos de la categoría general cuando la compra es grande.

El carrito lo rellena un formulario antiguo y a veces llegan líneas incorrectas. El programa no debe fallar por ellas: avisa de cada una y factura el resto.

Como en cualquier programa que maneje dinero, los importes se calculan en céntimos enteros. Sumar euros con decimales en coma flotante acumula pequeños errores (0.1 + 0.2 no es exactamente 0.3), y en una factura un céntimo de diferencia es un error.

Requisitos

  • Cada línea del carrito es nombre;categoria;precio;unidades. El precio no lleva IVA y usa punto decimal. Se ignoran las líneas vacías y los espacios al principio y al final de cada campo.

  • Una línea no es válida si no tiene exactamente 4 campos, si la categoría no es libros, alimentacion ni general, si el precio no es un número mayor o igual que 0 o si las unidades no son un número entero mayor que 0. Por cada una escribe Aviso: línea N ignorada, con N el número de línea de la entrada (las vacías también cuentan). Los avisos salen antes que la factura.

  • Si no queda ningún producto válido, escribe Carrito vacío y termina.

  • La tabla empieza con la cabecera y tiene una línea por producto, en el orden del carrito: el nombre en 22 caracteres alineado a la izquierda (contando letras, no bytes: «Lámpara» ocupa 7), y alineados a la derecha las unidades en 5 caracteres, el precio unitario en 10 y el importe (unidades × precio) en 11. La cabecera usa las mismas anchuras: Producto, Uds, Precio e Importe.

  • 3x2 en alimentación: por cada 3 unidades de un mismo producto de alimentación, una es gratis (unidades gratis = unidades ÷ 3, división entera). Por cada producto con unidades gratis, escribe 3x2 en nombre: -X €.

  • Descuento por importe: si la base de todo el carrito, ya con el 3x2 aplicado, supera los 100 € (estrictamente), los productos de la categoría general tienen un 5 % de descuento sobre su base, redondeado al céntimo. Si ese descuento es mayor que 0, escribe Descuento del 5 % en general: -X €.

  • Si hay alguna promoción, se escriben bajo la línea Promociones: con dos espacios delante: primero los 3x2 en el orden del carrito y después el descuento.

  • El resumen tiene la Base imponible: (la suma de las bases de las tres categorías tras las promociones), una línea IVA T %: por cada categoría con base mayor que 0, en el orden 4 %, 10 % y 21 %, y el TOTAL: (base más cuotas). La cuota de cada tipo es su base × T ÷ 100, redondeada al céntimo. Cada línea lleva la etiqueta en 16 caracteres a la izquierda, el importe en 12 a la derecha, un espacio y €.

  • Todos los importes se escriben con dos decimales, coma decimal y punto de millar, por ejemplo 1.200,00. Los redondeos son al céntimo más cercano y las mitades suben (como hace round en PHP).

Formato de la entrada

  • Una línea por producto: nombre;categoria;precio;unidades. Puede haber líneas vacías o mal escritas.

Categorías de la tienda
CategoríaIVAPromoción
libros4 %—
alimentacion10 %3x2 en cada producto
general21 %5 % de descuento si la base del carrito supera 100 €

Ejemplo: Un carrito con promociones

Entrada

Leche entera;alimentacion;0.95;6
Pan de molde;alimentacion;1.80;2
Auriculares;general;59.90;1
Cargador USB-C;general;19.95;2
El Quijote;libros;12.50;1

Salida esperada

Producto                Uds    Precio    Importe
Leche entera              6      0,95       5,70
Pan de molde              2      1,80       3,60
Auriculares               1     59,90      59,90
Cargador USB-C            2     19,95      39,90
El Quijote                1     12,50      12,50
Promociones:
  3x2 en Leche entera: -1,90 €
  Descuento del 5 % en general: -4,99 €
Base imponible:       114,71 €
IVA 4 %:                0,50 €
IVA 10 %:               0,74 €
IVA 21 %:              19,91 €
TOTAL:                135,86 €

Ejemplo: Con líneas incorrectas

Entrada

Yogur natural;alimentacion;0.45;7
Cuaderno;general;2.40;3
Revista;prensa;3.00;1
Bolígrafos;general;abc;2
Agua;alimentacion;0.30;0

Diccionario;libros;24.00;1

Salida esperada

Aviso: línea 3 ignorada
Aviso: línea 4 ignorada
Aviso: línea 5 ignorada
Producto                Uds    Precio    Importe
Yogur natural             7      0,45       3,15
Cuaderno                  3      2,40       7,20
Diccionario               1     24,00      24,00
Promociones:
  3x2 en Yogur natural: -0,90 €
Base imponible:        33,45 €
IVA 4 %:                0,96 €
IVA 10 %:               0,23 €
IVA 21 %:               1,51 €
TOTAL:                 36,15 €
Código de partida (php)
<?php// Tipos de IVA de cada categoríaconst IVA = ["libros" => 4, "alimentacion" => 10, "general" => 21]; /** Céntimos → «1.234,50» (punto de millar y coma decimal). */function dinero(int $centimos): string{    return number_format($centimos / 100, 2, ",", ".");} $lineas = explode("\n", stream_get_contents(STDIN));$productos = [];foreach ($lineas as $i => $linea) {    // TODO: valida la línea (o escribe el aviso) y guarda el producto con su precio en céntimos} // TODO: tabla de productos, promociones, base imponible, IVA de cada tipo y total

45. Préstamos de una biblioteca con PDO y transacciones

Muy difícil · PHP · 100 minutos · apuntesdam.com/ejercicios/php/biblioteca-con-pdo-y-transacciones

La biblioteca de un instituto quiere dejar la hoja de cálculo y gestionar los préstamos con una aplicación web en PHP. Antes de hacer las pantallas, se escribe y se prueba la capa de datos: funciones que reciben órdenes y trabajan contra la base de datos con PDO.

Para que el ejercicio funcione sin instalar nada, la base de datos es SQLite en memoria: el código inicial la crea con sus tres tablas y unos datos de partida, que tienes abajo. Con MySQL el código sería el mismo cambiando solo la cadena de conexión (el DSN): esa es la ventaja de PDO.

Las reglas del préstamo son las habituales: 15 días de plazo, como mucho 3 libros a la vez, nada nuevo si se tiene algo vencido y 0,20 € de multa por día de retraso. Comprobar las reglas y registrar el préstamo debe ser una operación atómica: si dos bibliotecarios prestan a la vez el último ejemplar, solo uno puede conseguirlo. Eso es una transacción.

Requisitos

  • Cada línea es una orden; antes de responder, escribe > y la línea tal como se ha leído (sin espacios en los extremos). Una orden desconocida o con argumentos que no tocan escribe Orden no válida.

  • BUSCAR texto: libros cuyo título o autor contienen el texto (sin distinguir mayúsculas), por orden de título, como código · título (autor) · disponibles D de E, donde D son los ejemplares menos los prestados sin devolver. Si no hay ninguno, Sin resultados para «texto».

  • PRESTAR código socio fecha: comprueba que existen el libro (No existe el libro X) y el socio (No existe el socio N) y, dentro de una transacción y en este orden, que queda algún ejemplar (No quedan ejemplares de «título»), que el socio no tiene préstamos activos vencidos a esa fecha (Nombre tiene préstamos vencidos) y que no tiene ya 3 activos (Nombre ya tiene 3 préstamos). Si todo va bien, registra el préstamo y escribe Préstamo N: «título» para Nombre (devolver antes del AAAA-MM-DD), con la fecha de salida más 15 días. Un préstamo vence cuando su fecha límite es anterior a la fecha de la operación.

  • DEVOLVER id fecha: No existe el préstamo N, El préstamo N ya se devolvió el AAAA-MM-DD, o registra la devolución y escribe Devuelto «título» a tiempo o Devuelto «título» con R días de retraso: multa X € (R = días transcurridos − 15; «día» si es 1).

  • MOROSOS fecha: Préstamos vencidos el fecha: y una línea por préstamo activo con retraso a esa fecha, por fecha de salida: Nombre: «título» (R días de retraso); o No hay préstamos vencidos el fecha.

  • RESUMEN: Préstamos activos: N y Socio más lector: Nombre (N préstamos) (contando también los devueltos; si empatan, el primero alfabéticamente).

  • Todas las consultas que llevan datos de la entrada deben ser consultas preparadas.

Formato de la entrada

  • Una orden por línea: BUSCAR, PRESTAR, DEVOLVER, MOROSOS o RESUMEN, con las fechas como AAAA-MM-DD.

libros
codigotituloautorejemplares
L01Cien años de soledadGabriel García Márquez2
L021984George Orwell1
L03Rebelión en la granjaGeorge Orwell2
L04La sombra del vientoCarlos Ruiz Zafón1
L05El nombre del vientoPatrick Rothfuss3
L06El tercer policíaFlann O'Brien1
socios
idnombre
1Ana Gil
2Luis Mora
3Eva Ruiz
prestamos
idcodigosociosalidadevolucion
1L0222026-09-01NULL
2L0112026-09-202026-09-28
3L0512026-09-25NULL

Ejemplo: Una mañana en la biblioteca

Entrada

BUSCAR viento
BUSCAR orwell
BUSCAR O'Brien
PRESTAR L02 3 2026-10-01
PRESTAR L04 2 2026-10-01
PRESTAR L04 3 2026-10-01
DEVOLVER 1 2026-10-01
PRESTAR L02 3 2026-10-02
MOROSOS 2026-10-20
RESUMEN

Salida esperada

> BUSCAR viento
L05 · El nombre del viento (Patrick Rothfuss) · disponibles 2 de 3
L04 · La sombra del viento (Carlos Ruiz Zafón) · disponibles 1 de 1
> BUSCAR orwell
L02 · 1984 (George Orwell) · disponibles 0 de 1
L03 · Rebelión en la granja (George Orwell) · disponibles 2 de 2
> BUSCAR O'Brien
L06 · El tercer policía (Flann O'Brien) · disponibles 1 de 1
> PRESTAR L02 3 2026-10-01
No quedan ejemplares de «1984»
> PRESTAR L04 2 2026-10-01
Luis Mora tiene préstamos vencidos
> PRESTAR L04 3 2026-10-01
Préstamo 4: «La sombra del viento» para Eva Ruiz (devolver antes del 2026-10-16)
> DEVOLVER 1 2026-10-01
Devuelto «1984» con 15 días de retraso: multa 3,00 €
> PRESTAR L02 3 2026-10-02
Préstamo 5: «1984» para Eva Ruiz (devolver antes del 2026-10-17)
> MOROSOS 2026-10-20
Préstamos vencidos el 2026-10-20:
  Ana Gil: «El nombre del viento» (10 días de retraso)
  Eva Ruiz: «La sombra del viento» (4 días de retraso)
  Eva Ruiz: «1984» (3 días de retraso)
> RESUMEN
Préstamos activos: 3
Socio más lector: Ana Gil (2 préstamos)

Ejemplo: Órdenes y datos que no existen

Entrada

BUSCAR Quijote
PRESTAR L99 1 2026-10-01
PRESTAR L01 7 2026-10-01
DEVOLVER 2 2026-10-01
DEVOLVER 50 2026-10-01
PRESTAR L01
CATALOGO

Salida esperada

> BUSCAR Quijote
Sin resultados para «Quijote»
> PRESTAR L99 1 2026-10-01
No existe el libro L99
> PRESTAR L01 7 2026-10-01
No existe el socio 7
> DEVOLVER 2 2026-10-01
El préstamo 2 ya se devolvió el 2026-09-28
> DEVOLVER 50 2026-10-01
No existe el préstamo 50
> PRESTAR L01
Orden no válida
> CATALOGO
Orden no válida
Código de partida (php)
<?php// Base de datos de la biblioteca (SQLite en memoria): tablas libros, socios y prestamos$pdo = new PDO("sqlite::memory:");$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);$pdo->exec(<<<SQL    CREATE TABLE libros (codigo TEXT PRIMARY KEY, titulo TEXT NOT NULL, autor TEXT NOT NULL, ejemplares INTEGER NOT NULL);    CREATE TABLE socios (id INTEGER PRIMARY KEY, nombre TEXT NOT NULL);    CREATE TABLE prestamos (        id INTEGER PRIMARY KEY AUTOINCREMENT,        codigo TEXT NOT NULL REFERENCES libros(codigo),        socio INTEGER NOT NULL REFERENCES socios(id),        salida TEXT NOT NULL,        devolucion TEXT    );    INSERT INTO libros VALUES        ('L01', 'Cien años de soledad', 'Gabriel García Márquez', 2),        ('L02', '1984', 'George Orwell', 1),        ('L03', 'Rebelión en la granja', 'George Orwell', 2),        ('L04', 'La sombra del viento', 'Carlos Ruiz Zafón', 1),        ('L05', 'El nombre del viento', 'Patrick Rothfuss', 3),        ('L06', 'El tercer policía', 'Flann O''Brien', 1);    INSERT INTO socios VALUES (1, 'Ana Gil'), (2, 'Luis Mora'), (3, 'Eva Ruiz');    INSERT INTO prestamos (codigo, socio, salida, devolucion) VALUES        ('L02', 2, '2026-09-01', NULL),        ('L01', 1, '2026-09-20', '2026-09-28'),        ('L05', 1, '2026-09-25', NULL);    SQL); const PLAZO = 15;          // días de préstamoconst MULTA = 20;          // céntimos por día de retrasoconst MAXIMO = 3;          // préstamos activos por socio function euros(int $c): string { return number_format($c / 100, 2, ",", ".") . " €"; } // TODO: buscar, prestar (en una transacción), devolver, morosos y resumen, siempre con consultas preparadas foreach (explode("\n", stream_get_contents(STDIN)) as $linea) {    $linea = trim($linea);    if ($linea === "") continue;    // TODO: escribe «> línea» y ejecuta la orden}

46. API REST de tareas con un router propio

Difícil · PHP · 90 minutos · apuntesdam.com/ejercicios/php/api-rest-de-tareas

Una aplicación móvil de tareas necesita un backend. La API sigue el estilo REST: la URL identifica el recurso (/api/tareas es la colección, /api/tareas/3 una tarea) y el método HTTP dice qué hacer con él. Las respuestas son JSON y el código de estado HTTP indica el resultado: 200 bien, 201 creado, 204 borrado sin contenido, 400 petición mal formada, 404 no existe, 405 método no permitido y 422 datos que no pasan la validación.

Los frameworks (Laravel, Symfony, Slim) traen un router que asocia cada ruta con su código. Aquí vas a escribir uno pequeño: cada ruta es un patrón como /api/tareas/{id} que se convierte en una expresión regular, y su acción es una closure.

Para probarlo sin servidor web, cada línea de la entrada es una petición HTTP simplificada (método, URL y cuerpo JSON), y el programa escribe la respuesta. Las tareas se guardan en memoria: una base de datos sería el siguiente paso.

Requisitos

  • Cada línea es MÉTODO URL [cuerpo JSON] (el cuerpo es todo lo que va tras el segundo espacio). Si la línea no tiene al menos método y URL, escribe Petición no válida. La respuesta se escribe como MÉTODO URL → estado cuerpo, con el método en mayúsculas, la URL tal como venía y el cuerpo en JSON compacto sin escapar tildes ni barras; las respuestas 204 no llevan cuerpo. Una barra al final de la ruta no cuenta (/api/tareas/ es /api/tareas).

  • Una tarea es {"id": n, "titulo": texto, "prioridad": alta|media|baja, "hecha": booleano}. Los id empiezan en 1 y no se reutilizan.

  • GET /api/tareas → 200 con el array de tareas por orden de id; admite los filtros ?hecha=true|false y ?q=texto (título que contiene el texto sin distinguir mayúsculas), combinables. GET /api/tareas/{id} → 200 con la tarea.

  • POST /api/tareas crea una tarea → 201 con la tarea. El título es obligatorio y, como los demás campos, se valida: título de 3 a 60 caracteres (tras quitar espacios de los extremos, que no se guardan), prioridad alta, media o baja (por defecto media), hecha true o false (por defecto false), y ningún otro campo (campo desconocido). Los errores devuelven 422 con {"errores": {campo: mensaje}}: obligatorio, entre 3 y 60 caracteres, debe ser alta, media o baja, debe ser true o false.

  • PATCH /api/tareas/{id} cambia solo los campos que vienen (con las mismas reglas, sin título obligatorio) → 200 con la tarea completa. DELETE /api/tareas/{id} → 204.

  • Si el cuerpo de un POST o PATCH no es un objeto JSON → 400 {"error": "JSON no válido"}. Si la tarea no existe → 404 {"error": "No existe la tarea N"}. Si la ruta no existe → 404 {"error": "Ruta no encontrada"}; si existe pero no con ese método → 405 {"error": "Método no permitido"}.

Formato de la entrada

  • Una petición por línea: MÉTODO URL y, en POST y PATCH, el cuerpo JSON.

Rutas de la API
MétodoRutaRespuesta correcta
GET/api/tareas?hecha=&q=200 + lista
POST/api/tareas201 + tarea creada
GET/api/tareas/{id}200 + tarea
PATCH/api/tareas/{id}200 + tarea modificada
DELETE/api/tareas/{id}204 sin cuerpo

Ejemplo: Crear, consultar, modificar y borrar

Entrada

POST /api/tareas {"titulo": "Preparar el examen de DWES", "prioridad": "alta"}
POST /api/tareas {"titulo": "  Comprar café  "}
GET /api/tareas
PATCH /api/tareas/2 {"hecha": true}
GET /api/tareas?hecha=true
GET /api/tareas?q=EXAMEN
DELETE /api/tareas/1
GET /api/tareas/1
GET /api/tareas/

Salida esperada

POST /api/tareas → 201 {"id":1,"titulo":"Preparar el examen de DWES","prioridad":"alta","hecha":false}
POST /api/tareas → 201 {"id":2,"titulo":"Comprar café","prioridad":"media","hecha":false}
GET /api/tareas → 200 [{"id":1,"titulo":"Preparar el examen de DWES","prioridad":"alta","hecha":false},{"id":2,"titulo":"Comprar café","prioridad":"media","hecha":false}]
PATCH /api/tareas/2 → 200 {"id":2,"titulo":"Comprar café","prioridad":"media","hecha":true}
GET /api/tareas?hecha=true → 200 [{"id":2,"titulo":"Comprar café","prioridad":"media","hecha":true}]
GET /api/tareas?q=EXAMEN → 200 [{"id":1,"titulo":"Preparar el examen de DWES","prioridad":"alta","hecha":false}]
DELETE /api/tareas/1 → 204
GET /api/tareas/1 → 404 {"error":"No existe la tarea 1"}
GET /api/tareas/ → 200 [{"id":2,"titulo":"Comprar café","prioridad":"media","hecha":true}]

Ejemplo: Errores de la API

Entrada

POST /api/tareas {"titulo": "ab", "prioridad": "urgente", "color": "rojo"}
POST /api/tareas {titulo: mal}
POST /api/tareas [1, 2]
POST /api/tareas {"prioridad": "baja"}
PUT /api/tareas
GET /api/usuarios
PATCH /api/tareas/9 {"hecha": true}
POST

Salida esperada

POST /api/tareas → 422 {"errores":{"titulo":"entre 3 y 60 caracteres","prioridad":"debe ser alta, media o baja","color":"campo desconocido"}}
POST /api/tareas → 400 {"error":"JSON no válido"}
POST /api/tareas → 400 {"error":"JSON no válido"}
POST /api/tareas → 422 {"errores":{"titulo":"obligatorio"}}
PUT /api/tareas → 405 {"error":"Método no permitido"}
GET /api/usuarios → 404 {"error":"Ruta no encontrada"}
PATCH /api/tareas/9 → 404 {"error":"No existe la tarea 9"}
Petición no válida
Código de partida (php)
<?php/** Una ruta: método, patrón con {parámetros} y la función que la atiende. */final class Ruta {    public function __construct(public string $metodo, public string $patron, public Closure $accion) {}     /** Los parámetros de la ruta si la URL encaja con el patrón, o null. */    public function encaja(string $ruta): ?array {        // TODO: convierte el patrón en una expresión regular con grupos con nombre        return null;    }} final class Respuesta {    public function __construct(public int $estado, public mixed $cuerpo = null) {}} final class Router {    private array $rutas = [];     public function add(string $metodo, string $patron, Closure $accion): void {        $this->rutas[] = new Ruta($metodo, $patron, $accion);    }     public function despachar(string $metodo, string $ruta, array $consulta, ?string $cuerpo): Respuesta {        // TODO: 404 si ninguna ruta encaja, 405 si encaja pero con otro método        return new Respuesta(404, ["error" => "Ruta no encontrada"]);    }} $tareas = [];$siguiente = 1; // TODO: registra las rutas GET, POST, PATCH y DELETE de /api/tareas y atiende cada línea de la entrada

47. Libro de visitas seguro: validación, spam y escape de HTML

Medio · PHP · 55 minutos · apuntesdam.com/ejercicios/php/libro-de-visitas-seguro

La web de una casa rural tiene un libro de visitas: los huéspedes dejan su nombre, su email, su web (opcional) y un mensaje, y los mensajes se publican en la página. Es el ejemplo perfecto de por qué no hay que fiarse de nada de lo que llega en un formulario.

Un visitante malintencionado puede escribir <script> en el mensaje o poner javascript:alert(1) como web: si se publica tal cual, el código se ejecuta en el navegador de todos los que vean la página (un ataque XSS). La defensa es doble: validar al recibir y escapar al mostrar con htmlspecialchars.

Además, los robots de spam rellenan todos los campos que encuentran. Un truco eficaz es el campo trampa (honeypot): un campo telefono2 que se oculta con CSS, que ninguna persona ve ni rellena. Si llega con algo, el envío es de un robot.

Requisitos

  • Cada línea no vacía es un envío del formulario codificado como lo envía el navegador (nombre=Ana&email=ana%40correo.es&mensaje=Hola), con los campos nombre, email, web, mensaje y telefono2. Los envíos se numeran desde 1. A todos los campos se les quitan los espacios de los extremos.

  • Si telefono2 no está vacío, escribe Mensaje N descartado: spam y no se valida nada más.

  • Reglas, en este orden: nombre obligatorio y de 2 a 50 caracteres; email obligatorio y válido según FILTER_VALIDATE_EMAIL; web opcional, pero si se escribe tiene que ser una URL válida con esquema http o https (solo direcciones http o https); mensaje obligatorio y de 200 caracteres como mucho. Los mensajes de error son obligatorio, entre 2 y 50 caracteres, no es un email válido, solo direcciones http o https y como mucho 200 caracteres.

  • Un email (sin distinguir mayúsculas) solo puede publicar 2 mensajes: si el envío es correcto pero ese email ya tiene 2 publicados, el error es email: ya ha enviado 2 mensajes.

  • Por cada error, Mensaje N rechazado: campo: motivo. Al final, la lista HTML de los mensajes publicados en orden: <ul class="visitas">, una línea por mensaje <li><strong>nombre</strong> (<a href="web" rel="nofollow ugc">web</a>): mensaje</li> (sin el paréntesis si no hay web) y </ul>, con el nombre, la web y el mensaje escapados con htmlspecialchars (ENT_QUOTES | ENT_HTML5). Después, Publicados: P · Rechazados: R · Spam: S.

Formato de la entrada

  • Un envío del formulario por línea, como query string (campo=valor&campo=valor).

Ejemplo: Cinco envíos

Entrada

nombre=Ana+Gil&email=ana%40correo.es&web=https%3A%2F%2Fana.es&mensaje=Un+sitio+precioso%2C+volveremos+%3C3&telefono2=
nombre=Hacker&email=h%40x.com&web=javascript%3Aalert(1)&mensaje=%3Cscript%3Ealert(1)%3C%2Fscript%3E&telefono2=
nombre=Bot&email=bot%40spam.com&mensaje=Compra+ya&telefono2=600123123
nombre=Seán+O%27Brien&email=SEAN%40mail.ie&mensaje=%3Cb%3EGreat%3C%2Fb%3E+%26+quiet&telefono2=
nombre=A&email=no-es-email&mensaje=

Salida esperada

Mensaje 2 rechazado: web: solo direcciones http o https
Mensaje 3 descartado: spam
Mensaje 5 rechazado: nombre: entre 2 y 50 caracteres
Mensaje 5 rechazado: email: no es un email válido
Mensaje 5 rechazado: mensaje: obligatorio
<ul class="visitas">
  <li><strong>Ana Gil</strong> (<a href="https://ana.es" rel="nofollow ugc">web</a>): Un sitio precioso, volveremos &lt;3</li>
  <li><strong>Seán O&apos;Brien</strong>: &lt;b&gt;Great&lt;/b&gt; &amp; quiet</li>
</ul>
Publicados: 2 · Rechazados: 2 · Spam: 1

Ejemplo: Límite de mensajes por email

Entrada

nombre=Eva&email=eva%40x.es&mensaje=Uno
nombre=Eva&email=EVA%40x.es&mensaje=Dos
nombre=Eva&email=eva%40x.es&mensaje=Tres

Salida esperada

Mensaje 3 rechazado: email: ya ha enviado 2 mensajes
<ul class="visitas">
  <li><strong>Eva</strong>: Uno</li>
  <li><strong>Eva</strong>: Dos</li>
</ul>
Publicados: 2 · Rechazados: 1 · Spam: 0
Código de partida (php)
<?phpconst MAX_POR_EMAIL = 2; /** Errores de un mensaje (campo → motivo), en el orden de los campos. */function validar(array $f): array {    $e = [];    // TODO: nombre, email, web y mensaje    return $e;} /** Escapa para HTML: <, >, &, comillas dobles y simples. */function h(string $texto): string {    // TODO    return $texto;} // TODO: lee cada envío con parse_str, descarta el spam, valida y escribe la lista HTML y el resumen

48. Horas trabajadas con nocturnidad usando DateTime

Difícil · PHP · 70 minutos · apuntesdam.com/ejercicios/php/horas-trabajadas-y-nocturnidad

Un hospital paga a su personal de guardias por horas, con un plus del 25 % por cada hora trabajada de noche (de 22:00 a 06:00). Los fichajes se exportan del control de presencia con la entrada y la salida de cada turno, y a fin de mes hay que calcular cuánto cobra cada persona.

Las fechas son la parte delicada: un turno de 22:00 a 06:30 cruza la medianoche, y uno de 20:00 a 08:00 tiene unas horas nocturnas y otras no. Calcularlo con horas sueltas lleva a errores; con objetos de fecha y la intersección de intervalos sale solo.

El control de presencia comete errores: fechas que no existen, salidas anteriores a la entrada, turnos que se olvidaron de cerrar (de más de 12 horas) o fichajes duplicados que se solapan. Esos turnos se rechazan con un mensaje y no se pagan.

Requisitos

  • La primera línea es TARIFA euros (euros por hora, con hasta dos decimales); si no lo es, escribe Falta la tarifa y nada más. Cada línea siguiente es un turno: nombre;AAAA-MM-DD HH:MM;AAAA-MM-DD HH:MM (entrada y salida).

  • Un turno se rechaza, escribiendo Línea N: motivo (N es el número de línea de la entrada), si: no tiene ese formato o alguna fecha no existe (formato no válido); la salida no es posterior a la entrada (la salida no es posterior a la entrada); dura más de 12 horas (un turno no puede pasar de 12 horas); o se solapa con un turno ya aceptado de la misma persona (se solapa con otro turno de nombre). Dos turnos que solo se tocan (uno termina cuando empieza el otro) no se solapan.

  • Horas nocturnas: los minutos del turno que caen entre las 22:00 y las 06:00 de cualquier noche.

  • Importe de cada persona: todos sus minutos a la tarifa más un 25 % de la tarifa por cada minuto nocturno, redondeado al céntimo.

  • Escribe una línea por persona, por orden alfabético: nombre: T turnos, D (nocturnas N) · importe, con «1 turno» en singular y las duraciones como 38 h 30 min o 8 h. Al final, Total a pagar: X. Los importes con punto de millar, coma decimal y €.

Formato de la entrada

  • Línea 1: TARIFA euros. Resto: nombre;entrada;salida con fechas AAAA-MM-DD HH:MM.

Ejemplo: Guardias de una semana

Entrada

TARIFA 14.50
Ana Gil;2026-09-28 22:00;2026-09-29 06:30
Luis Mora;2026-09-28 08:00;2026-09-28 15:00
Ana Gil;2026-09-30 20:00;2026-10-01 08:00
Luis Mora;2026-09-29 15:00;2026-09-29 23:15

Salida esperada

Ana Gil: 2 turnos, 20 h 30 min (nocturnas 16 h) · 355,25 €
Luis Mora: 2 turnos, 15 h 15 min (nocturnas 1 h 15 min) · 225,66 €
Total a pagar: 580,91 €

Ejemplo: Turnos rechazados

Entrada

TARIFA 12
Eva;2026-09-31 08:00;2026-09-31 14:00
Eva;2026-10-01 14:00;2026-10-01 08:00
Eva;2026-10-01 08:00;2026-10-01 21:00
Eva;2026-10-02 08:00;2026-10-02 14:00
Eva;2026-10-02 13:00;2026-10-02 18:00
Eva;2026-10-02 14:00;2026-10-02 18:00
Eva sin fechas

Salida esperada

Línea 2: formato no válido
Línea 3: la salida no es posterior a la entrada
Línea 4: un turno no puede pasar de 12 horas
Línea 6: se solapa con otro turno de Eva
Línea 8: formato no válido
Eva: 2 turnos, 10 h (nocturnas 0 h) · 120,00 €
Total a pagar: 120,00 €
Código de partida (php)
<?php/** Minutos de [$inicio, $fin) que caen entre las 22:00 y las 06:00. */function minutosNocturnos(DateTimeImmutable $inicio, DateTimeImmutable $fin): int {    // TODO    return 0;} function euros(int $c): string {    return number_format($c / 100, 2, ",", ".") . " €";} $lineas = explode("\n", stream_get_contents(STDIN));// TODO: lee la tarifa y los turnos, valídalos y escribe las horas y el importe de cada persona