Apuntes DAM
Tema 7 de 8DWES · 2º DAW

Desarrollo Web en Entorno Servidor

Aplicaciones Dinámicas e Híbridas

Endpoints para AJAX (JSON y fragmentos), negociación de contenido, HTMX, tiempo real con SSE y WebSockets, mashups con mapas, datos abiertos y APIs, pagos con webhooks firmados, inicio de sesión con OAuth, robustez frente a servicios externos y pruebas.

90 min lecturaAvanzadoRevisado el

1. Páginas que se actualizan sin recargarse

En Desarrollo Web en Entorno Cliente aprendiste a pedir datos con fetch sin recargar la página. Este tema es la otra mitad de esa conversación: qué debe hacer el servidor para que esas peticiones funcionen bien, y cómo combinar tu aplicación con servicios de otros (mapas, pagos, tiempo, inicio de sesión con Google) para construir aplicaciones híbridas.

Para el servidor, una petición AJAX es una petición HTTP como cualquier otra. La diferencia está en lo que responde: en lugar de una página completa, devuelve solo lo necesario, ya sea un JSON con datos que el JavaScript del navegador convertirá en HTML, o un fragmento de HTML ya hecho que se inserta en la página tal cual.

EstrategiaEl servidor devuelveVentajasInconvenientes
JSON + JavaScriptDatosEl cliente tiene el control total; la misma API sirve a la app móvilLa plantilla se duplica en JavaScript
Fragmentos de HTML (HTMX, Turbo, Livewire)Trozos de HTML renderizados con tus plantillasCasi sin JavaScript; una sola plantillaMenos adecuado para interfaces muy interactivas
Página completaTodo el documentoLo más simpleRecarga todo; experiencia más lenta

Una misma ruta, varios formatos

El navegador indica qué prefiere con la cabecera Accept. Así, una ruta puede devolver la página completa cuando se visita normalmente y JSON o un fragmento cuando la pide el JavaScript. Es la negociación de contenido, y permite que la página funcione también sin JavaScript (mejora progresiva).

php
1$productos = $repo->buscar($_GET['q'] ?? '');
2
3$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
4if (str_contains($accept, 'application/json')) {
5    header('Content-Type: application/json; charset=utf-8');
6    echo json_encode($productos, JSON_UNESCAPED_UNICODE);
7} elseif (isset($_SERVER['HTTP_HX_REQUEST'])) {          // la petición la hace HTMX
8    echo $vista->render('productos/_lista', ['productos' => $productos], layout: null);
9} else {
10    echo $vista->render('productos/index', ['productos' => $productos]);
11}

2. Endpoints para interfaces interactivas

Estas son las piezas que más a menudo te pedirán en una aplicación real. Todas siguen el mismo patrón: el navegador envía una petición pequeña, el servidor valida, consulta y responde rápido.

InteracciónPeticiónRespuesta del servidor
Validación en vivo (¿email libre?)GET /api/usuarios/disponible?email=…{ disponible: true|false }
AutocompletadoGET /api/municipios?q=logArray de sugerencias (máx. 5-10)
Scroll infinito o «Ver más»GET /api/articulos?despues=128Siguientes elementos y si hay más
Guardado automáticoPATCH /api/borradores/7 cada pocos segundos204 o la versión guardada
«Me gusta», añadir al carritoPOST /api/articulos/7/me-gustaEl nuevo total (para la actualización optimista)
Subida de ficheros con progresoPOST multipart con XMLHttpRequestLa URL del fichero guardado
api/disponible.php: validación en vivo
1<?php
2header('Content-Type: application/json; charset=utf-8');
3header('Cache-Control: no-store');
4
5$email = trim($_GET['email'] ?? '');
6if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
7    http_response_code(422);
8    echo json_encode(['disponible' => false, 'motivo' => 'formato']);
9    exit;
10}
11$s = conectar()->prepare('SELECT 1 FROM usuarios WHERE email = ?');
12$s->execute([$email]);
13echo json_encode(['disponible' => !$s->fetchColumn()]);
registro.js (cliente)
1const campo = document.querySelector("#email");
2const aviso = document.querySelector("#aviso-email");
3let espera;
4
5campo.addEventListener("input", () => {
6  clearTimeout(espera);
7  espera = setTimeout(async () => {                    // espera a que deje de escribir
8    const r = await fetch("/api/disponible.php?email=" + encodeURIComponent(campo.value), {
9      headers: { Accept: "application/json" },
10    });
11    const { disponible } = await r.json();
12    aviso.textContent = disponible ? "" : "Ese email ya está registrado";
13  }, 400);
14});

La validación en vivo no sustituye a la del formulario

Mejora la experiencia, pero el usuario puede saltársela. Al enviar el formulario, el servidor vuelve a comprobarlo todo (y el índice UNIQUE de la base de datos es la garantía final). Ten en cuenta además que un endpoint de «¿existe este email?» permite enumerar usuarios: limita sus peticiones.

3. Modificar la estructura de la página con fragmentos: HTMX

HTMX es una pequeña librería que devuelve el protagonismo al servidor: añades atributos al HTML que dicen qué petición hacer, cuándo y dónde colocar la respuesta, y el servidor devuelve fragmentos de HTML generados con tus plantillas de siempre. Es una forma muy productiva de hacer interfaces dinámicas con PHP, y la misma idea está detrás de Laravel Livewire y de Hotwire (Ruby on Rails).

Buscador en vivo con HTMX
1<script src="https://unpkg.com/htmx.org@2"></script>
2
3<input type="search" name="q" placeholder="Buscar productos…"
4       hx-get="/productos" hx-trigger="input changed delay:300ms" hx-target="#resultados">
5<div id="resultados"><!-- aquí el servidor coloca el fragmento --></div>
6
7<button hx-post="/carrito/7" hx-target="#contador-carrito" hx-swap="outerHTML">
8  Añadir al carrito
9</button>
plantillas/productos/_lista.php (el fragmento)
1<?php if (!$productos): ?>
2  <p>No hay resultados para «<?= e($q) ?>».</p>
3<?php endif ?>
4<?php foreach ($productos as $p): ?>
5  <article class="producto">
6    <h3><?= e($p['nombre']) ?></h3>
7    <p><?= number_format($p['precio'], 2, ',', '.') ?> €</p>
8  </article>
9<?php endforeach ?>
Aprende HTMX en 10 minutos (The Coder Cave)

4. Información en tiempo real desde PHP

El modelo de PHP (un proceso por petición que termina enseguida) no está pensado para conexiones que duran horas, pero hay opciones para cada necesidad:

TécnicaEn PHPÚsala para
Sondeo (polling)Un endpoint normal que el cliente llama cada N segundosDatos que cambian poco; lo más sencillo
Server-Sent EventsUn script que responde text/event-stream y va enviando eventosNotificaciones, progreso de una tarea, marcadores
WebSocketsUn servidor aparte en bucle de eventos: Ratchet, Laravel Reverb, Soketi, o un servicio (Pusher, Ably)Chats, colaboración, juegos
progreso.php: Server-Sent Events
1<?php
2header('Content-Type: text/event-stream');
3header('Cache-Control: no-store');
4header('X-Accel-Buffering: no');                  // que Nginx no acumule la salida
5session_write_close();                            // libera la sesión: si no, bloquea las demás peticiones
6
7for ($i = 1; $i <= 10 && !connection_aborted(); $i++) {
8    $estado = consultarProgresoExportacion($_GET['tarea']);
9    echo "event: progreso\n";
10    echo 'data: ' . json_encode(['porcentaje' => $estado['porcentaje']]) . "\n\n";
11    @ob_flush();
12    flush();                                      // envía ya, sin esperar al final del script
13    if ($estado['porcentaje'] >= 100) break;
14    sleep(1);
15}
16
17// En el cliente: new EventSource('/progreso.php?tarea=42').addEventListener('progreso', …)

Cada conexión abierta ocupa un proceso

Con PHP-FPM, cada cliente conectado por SSE mantiene ocupado un proceso mientras dura el bucle. Con decenas de usuarios no importa; con miles, conviene un servidor de eventos especializado (Mercure, o los de WebSockets citados) al que PHP publica los mensajes.

5. Aplicaciones híbridas: reutilizar código e información

Ninguna aplicación moderna se construye desde cero. Se reutiliza código (paquetes de Composer y npm) y se reutiliza información y funciones de otros servicios a través de sus APIs. Combinar datos propios con los de servicios externos da lugar a las aplicaciones híbridas o mashups: la web de un hotel con el mapa de OpenStreetMap, el tiempo de la zona, las reseñas de Google y el pago con Stripe.

Una aplicación híbrida es una cocina con proveedores

Tu cocina
Tu aplicación: los datos propios y las reglas del negocio, lo que te diferencia.
Los proveedores
Mapas, pagos, correo, tiempo: servicios especializados que contratas en lugar de fabricarlos.
Los contratos
Sus APIs, con condiciones de uso, límites de peticiones y, a veces, precio.
NecesidadServicios habitualesCómo se integra
MapasOpenStreetMap + Leaflet, Google Maps, MapboxLibrería JavaScript en el cliente; geocodificación desde el servidor
PagosStripe, Redsys, PayPal, Bizum (vía pasarela)Página de pago del proveedor + webhook firmado
CorreoSMTP, Brevo, Mailgun, Amazon SESLibrería (Symfony Mailer, PHPMailer) con cola de envío
Inicio de sesiónGoogle, GitHub, Microsoft (OAuth 2.0 / OpenID Connect)Redirección, código de autorización y token
Datos abiertosdatos.gob.es, AEMET OpenData, INE, Open-MeteoAPI REST con caché en tu servidor
Contenido embebidoYouTube, Vimeo, redes socialesIframes o widgets (ojo con las cookies de terceros)
FicherosAmazon S3, Cloudflare R2SDK o Flysystem; URLs firmadas temporales

Consumir una API desde el servidor

php
1// Con funciones nativas
2$contexto = stream_context_create(['http' => ['timeout' => 5, 'header' => "Accept: application/json\r\n"]]);
3$respuesta = @file_get_contents('https://api.open-meteo.com/v1/forecast?latitude=42.23&longitude=-2.10&current=temperature_2m', false, $contexto);
4if ($respuesta === false) {
5    // plan B: mostrar la página sin el tiempo, nunca un error en blanco
6}
7$tiempo = json_decode($respuesta, true);
8
9// Con Guzzle (composer require guzzlehttp/guzzle), más cómodo para APIs complejas
10$cliente = new GuzzleHttp\Client(['base_uri' => 'https://api.ejemplo.com/', 'timeout' => 5]);
11$datos = json_decode($cliente->get('pedidos', ['query' => ['estado' => 'pendiente']])->getBody(), true);
Cómo integrar mapas con Leaflet y OpenStreetMap (Códigos de Programación - MR)

6. Incorporar funcionalidades específicas: pagos, webhooks e inicio de sesión

Un pago con pasarela

Nunca proceses tú los datos de la tarjeta: la pasarela te ofrece una página o un formulario alojado por ella, y así tu servidor no toca datos de pago (y no tienes que cumplir la exigente normativa PCI DSS). El flujo típico combina una redirección con un aviso de servidor a servidor:

  1. 01

    Crear el pago

    Tu servidor crea una sesión de pago en la pasarela (importe, pedido, URL de vuelta) con su clave secreta.

  2. 02

    Página de la pasarela

    El usuario paga en la página del proveedor, que gestiona la tarjeta, Bizum o la autenticación del banco.

  3. 03

    Vuelta a tu web

    El navegador vuelve a tu URL de éxito. ¡No te fíes de ella para dar el pedido por pagado!

  4. 04

    Webhook

    La pasarela avisa a tu servidor con una petición firmada: esa es la confirmación real del pago.

  5. 05

    Marcar pagado

    Verificas la firma, compruebas que no lo procesaste ya (idempotencia) y actualizas el pedido.

webhooks/pagos.php
1<?php
2$cuerpo = file_get_contents('php://input');                 // el cuerpo crudo: la firma se calcula sobre él
3$firma = $_SERVER['HTTP_FIRMA'] ?? '';
4
5if (!firmaValida($firma, $cuerpo, getenv('PAGOS_WEBHOOK_SECRETO'))) {   // el reto del tema
6    http_response_code(400);
7    exit;
8}
9$evento = json_decode($cuerpo, true);
10
11if ($evento['tipo'] === 'pago.completado') {
12    $s = conectar()->prepare("UPDATE pedidos SET estado = 'pagado' WHERE id = ? AND estado = 'pendiente'");
13    $s->execute([$evento['pedido']]);                        // si ya estaba pagado, no hace nada: idempotente
14}
15http_response_code(200);                                    // responder rápido: si no, la pasarela reintenta
¿Qué es un Webhook y para qué sirve? (Oscar Fernández)

Iniciar sesión con Google (OAuth 2.0 y OpenID Connect)

  1. Tu web redirige al usuario a Google con tu identificador de cliente, los permisos pedidos (openid email profile), la URL de vuelta y un state aleatorio guardado en la sesión (protege de CSRF).
  2. El usuario inicia sesión en Google (tú nunca ves su contraseña) y acepta.
  3. Google redirige a tu URL de vuelta con un code de un solo uso y el mismo state, que compruebas.
  4. Tu servidor canjea el code (con tu secreto de cliente, de servidor a servidor) por un id_token: un JWT firmado por Google con el email y el identificador del usuario.
  5. Validas el token (firma con las claves públicas de Google, emisor, audiencia y caducidad), buscas o creas el usuario y abres tu sesión normal.

En la práctica se usa una librería probada (league/oauth2-client, Laravel Socialite) en lugar de implementarlo a mano: los detalles de seguridad son muchos y delicados.

7. Depender de otros sin sufrir: robustez, legalidad y pruebas

Cada servicio externo es una pieza que puede fallar, ir lenta, cambiar su API o subir el precio. Estas prácticas convierten esos fallos en molestias menores:

  • Tiempo máximo en todas las llamadas: si el otro servicio se cuelga, tu página no debe colgarse con él.
  • Caché de las respuestas que cambian poco (el tiempo, un tipo de cambio, la geolocalización de una dirección).
  • Reintentos con espera creciente solo para errores pasajeros (timeouts, 502, 503, 429) y solo en operaciones idempotentes.
  • Cortacircuitos: si el servicio falla repetidamente, deja de llamarlo un tiempo (el reto más difícil del tema).
  • Trabajo en segundo plano: enviar correos o llamar a APIs lentas desde una cola, no dentro de la petición del usuario.
  • Degradación elegante: sin mapa o sin tiempo, la página debe seguir funcionando.
  • Datos externos como no confiables: valida lo que recibes y escápalo al mostrarlo.
  • Claves en variables de entorno, nunca en el código ni en el JavaScript del cliente.

Protección de datos

Enviar datos personales a un tercero (el email a un servicio de correo, la dirección a uno de mapas) es un tratamiento de datos que debe figurar en tu política de privacidad, con un contrato de encargado del tratamiento y cuidado con las transferencias fuera de la UE. Y los contenidos embebidos (vídeos, mapas de Google, botones sociales) instalan cookies de terceros que requieren consentimiento.

Probar sin depender de la red

php
1interface ServicioTiempo
2{
3    /** @return array{temperatura: float, cielo: string}|null */
4    public function actual(float $lat, float $lon): ?array;
5}
6
7final class OpenMeteo implements ServicioTiempo { /* llamada HTTP real con timeout y caché */ }
8
9final class TiempoFalso implements ServicioTiempo                  // para los tests
10{
11    public function __construct(private ?array $respuesta) {}
12    public function actual(float $lat, float $lon): ?array { return $this->respuesta; }
13}
14
15// El test comprueba que la ficha de la tienda se muestra aunque el servicio falle
16$html = (new FichaTiendaControlador(new TiempoFalso(null)))->mostrar(1);
17$this->assertStringContainsString('Calle Mayor', $html);
18$this->assertStringNotContainsString('°C', $html);

Para probar a mano, los proveedores ofrecen entornos de pruebas (sandbox) con claves de test y tarjetas ficticias, y herramientas para enviar webhooks de prueba a tu máquina (Stripe CLI, o un túnel como ngrok).

8. Ejemplo resuelto: la ficha de una tienda física

Una cadena de tiendas quiere, para cada local, una página con su dirección en un mapa, el tiempo que hace ahora en esa ciudad y un formulario de contacto que valide el email mientras se escribe. Combinamos datos propios (las tiendas), dos servicios externos gratuitos y un endpoint AJAX.

  1. Datos propios: la tienda (nombre, dirección, latitud, longitud) sale de nuestra base de datos.
  2. El tiempo se pide a Open-Meteo desde el servidor, con timeout de 3 s y caché de 10 minutos por tienda. Si falla, la página se muestra sin ese bloque.
  3. El mapa lo dibuja Leaflet en el navegador con los mosaicos de OpenStreetMap (con su atribución obligatoria); el servidor solo pasa las coordenadas.
  4. El formulario usa el endpoint de validación en vivo y, al enviarse, se valida de nuevo en el servidor.
src/Tiempo/OpenMeteo.php
1final class OpenMeteo implements ServicioTiempo
2{
3    public function __construct(private Cache $cache) {}
4
5    public function actual(float $lat, float $lon): ?array
6    {
7        return $this->cache->recordar("tiempo:$lat:$lon", 600, function () use ($lat, $lon) {
8            $url = 'https://api.open-meteo.com/v1/forecast?' . http_build_query([
9                'latitude' => $lat, 'longitude' => $lon, 'current' => 'temperature_2m,weather_code',
10            ]);
11            $ctx = stream_context_create(['http' => ['timeout' => 3, 'ignore_errors' => true]]);
12            $json = @file_get_contents($url, false, $ctx);
13            $datos = $json ? json_decode($json, true) : null;
14            if (!isset($datos['current']['temperature_2m'])) {
15                return null;                               // se guarda en caché también el fallo, para no insistir
16            }
17            return [
18                'temperatura' => (float) $datos['current']['temperature_2m'],
19                'cielo' => ($datos['current']['weather_code'] ?? 0) < 3 ? 'despejado' : 'nublado',
20            ];
21        });
22    }
23}
plantillas/tiendas/ficha.php
1<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
2<h1><?= e($tienda['nombre']) ?></h1>
3<p><?= e($tienda['direccion']) ?></p>
4
5<?php if ($tiempo): ?>
6  <p class="tiempo">Ahora: <?= round($tiempo['temperatura']) ?> °C, <?= $tiempo['cielo'] ?></p>
7<?php endif ?>
8
9<div id="mapa" style="height: 320px" data-lat="<?= $tienda['lat'] ?>" data-lon="<?= $tienda['lon'] ?>"></div>
10<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
11<script>
12  const el = document.querySelector("#mapa");
13  const pos = [Number(el.dataset.lat), Number(el.dataset.lon)];
14  const mapa = L.map(el).setView(pos, 16);
15  L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
16    attribution: "&copy; colaboradores de OpenStreetMap",
17  }).addTo(mapa);
18  L.marker(pos).addTo(mapa);
19</script>

Fíjate en tres decisiones: el tiempo se pide desde el servidor (así se cachea para todos los visitantes y la clave, si la hubiera, no se expone); las coordenadas llegan al JavaScript en atributos data- (nada de escribir PHP dentro del script); y si Open-Meteo no responde, la ficha se muestra igual, solo que sin el tiempo.

9. Retos

Tres piezas del lado del servidor que encontrarás en casi cualquier aplicación híbrida: el endpoint de un autocompletado, la verificación de los avisos firmados de una pasarela de pagos y un cortacircuitos que protege tu aplicación cuando un servicio externo se cae.

🐘PHPEl endpoint de un autocompletadoMedio

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 [].

⏳
Prefijos y límite de 5
⏳
Mayúsculas, tildes y mínimo
⏳
Test oculto #3
0/3 tests pasados
🐘PHPVerificar la firma de un webhookDifícil

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».

⏳
Firma correcta y secretos en rotación
⏳
Ataques
⏳
Test oculto #3
0/3 tests pasados
🐘PHPUn cortacircuitos para un servicio externoMuy difícil

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».

⏳
Se abre y la prueba falla
⏳
Un éxito reinicia y la prueba sale bien
⏳
Test oculto #3
0/3 tests pasados

10. Ejercicios

Ejemplo Guiado
Fácil

¿JSON, fragmento o página?

Para cada caso, decide si el servidor debería devolver JSON, un fragmento de HTML o la página completa, y justifícalo: 1. Una app móvil pide la lista de pedidos. 2. Un buscador en vivo hecho con HTMX. 3. Un gráfico de ventas dibujado con Chart.js. 4. Un usuario abre un enlace compartido a un producto. 5. El botón «Ver más comentarios» de un blog hecho con PHP y plantillas.

Ejercicio Práctico
Medio

Scroll infinito con paginación por cursor

Crea /api/articulos que devuelva los 10 artículos más recientes y, si recibe ?antes_de=ID, los 10 siguientes más antiguos que ese id, junto con un campo mas (true/false). Después, en el cliente, carga más artículos cuando el usuario se acerque al final con IntersectionObserver. ¿Por qué el cursor (antes_de) es mejor aquí que OFFSET si mientras tanto se publican artículos nuevos?

Ejercicio Práctico
Medio

Buscador y carrito con HTMX

Sin escribir JavaScript propio, añade a un listado de productos en PHP: un buscador que filtra mientras escribes (300 ms tras la última tecla), un botón «Añadir al carrito» en cada producto que actualiza el contador de la cabecera, y un indicador de carga. Todo debe seguir funcionando con JavaScript desactivado (el formulario y los botones hacen peticiones normales).

Ejercicio Práctico
Difícil

Un mashup de datos abiertos

Elige un conjunto de datos abiertos de tu comunidad o ayuntamiento en datos.gob.es (bibliotecas, puntos de recarga, fuentes, aparcamientos…) y construye una página que lo muestre en un mapa de Leaflet con filtros, combinado con al menos otra fuente (el tiempo de Open-Meteo, la dirección obtenida por geocodificación inversa de Nominatim…). Descarga y guarda en caché los datos en tu servidor (no desde el navegador), respeta los límites de uso y la licencia de cada fuente y muéstralas en la página.

Ejercicio Práctico
Difícil

Correos en una cola

Al registrarse, un usuario debe recibir un correo de bienvenida. Enviarlo dentro de la petición hace que el registro tarde 2-3 segundos y falle si el servidor de correo no responde. Diseña e implementa una cola en base de datos: el registro solo inserta una tarea (tipo, datos en JSON, intentos, disponible_desde, estado); un script trabajador (php trabajador.php, ejecutado cada minuto o en bucle) toma tareas pendientes de forma segura aunque haya dos trabajadores a la vez, las ejecuta y, si fallan, las reprograma con espera creciente hasta 5 intentos.

Ejercicio Práctico
Muy difícil

Inicio de sesión con GitHub

Implementa «Entrar con GitHub» sin librerías, siguiendo el flujo OAuth 2.0 con código de autorización y PKCE: registra una OAuth App en GitHub; en /auth/github genera state y code_verifier, guarda ambos en la sesión y redirige a la página de autorización con code_challenge (S256); en /auth/github/vuelta comprueba el state, canjea el code (con client_secret y code_verifier) por un access_token, pide /user y /user/emails a la API de GitHub, busca o crea el usuario (vinculado por el id de GitHub, no por el email) y abre sesión. Explica qué ataque evita cada comprobación.

11. Errores típicos

  • Dar un pedido por pagado porque el navegador volvió a la URL de éxito: la confirmación es el webhook verificado.
  • Llamar a APIs externas sin timeout, o en cada visita sin caché.
  • Poner claves secretas en el JavaScript o en el repositorio.
  • Olvidar session_write_close() en peticiones largas (SSE, subidas): la sesión bloquea las demás peticiones del mismo usuario.
  • Mezclar PHP dentro de un script de JavaScript sin escapar: usa atributos data- o json_encode con JSON_HEX_TAG.
  • Endpoints AJAX sin protección: también necesitan autenticación, CSRF en las escrituras y límite de peticiones.
  • Embeber servicios de terceros sin avisar en la política de privacidad y de cookies.

Autoevaluación

0/10 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Qué debe devolver un endpoint PHP al que llama fetch para comprobar si un nombre de usuario está libre?

  2. 2.¿Qué propone HTMX?

  3. 3.¿Con qué tecnología puede PHP empujar notificaciones al navegador de forma sencilla, sin servidor adicional?

  4. 4.¿Qué es una aplicación web híbrida (mashup)?

  5. 5.¿Por qué hay que verificar la firma de un webhook?

  6. 6.Tu página llama a la API del tiempo, que tarda 30 segundos en responder cuando falla. ¿Qué haces?

  7. 7.¿Dónde debe guardarse la clave secreta de la API de pagos?

  8. 8.En el inicio de sesión con Google (OAuth 2.0 / OpenID Connect), ¿quién comprueba la contraseña del usuario?

  9. 9.¿Qué hace un cortacircuitos (circuit breaker)?

  10. 10.¿Cómo pruebas automáticamente un código que llama a una API de terceros?

¿Has encontrado un error, algo desactualizado o una explicación que no se entiende? Avísanos y lo corregimos.

¿Has terminado este tema?

Crea una cuenta gratis para guardar qué temas has terminado, subir de nivel y ganar medallas.

Guardar mi progreso