Un mashup en PHP: la agenda de actividades con la previsión del tiempo
Combina dos servicios web en una aplicación híbrida: la agenda de actividades de un ayuntamiento y la previsión de Open-Meteo, en JSON. Valida las dos respuestas, los cruza por fecha y avisa de las actividades al aire libre con riesgo de lluvia, sin romperse si un servicio falla.
- Aplicaciones híbridas (mashups)
- Consumir APIs JSON con json_decode
- Cruzar datos de dos servicios
- DateTime::createFromFormat
- usort con el operador nave espacial
- Degradación elegante ante errores
Enunciado
Una aplicación híbrida (mashup) junta datos de varios servicios web para dar algo que ninguno da por separado. Aquí, la agenda de actividades del ayuntamiento (con el formato de los portales de datos abiertos: una lista @graph con title, dtstart, event-location, free y price, más un campo aire-libre) y la previsión diaria de Open-Meteo (listas paralelas en daily: time, weathercode, temperaturas y probabilidad de lluvia).
En una aplicación real, las dos respuestas llegarían con file_get_contents o cURL; aquí llegan por la entrada, separadas por una línea ---, para poder probar también qué pasa cuando un servicio falla. Un mashup bien hecho no se cae si una de sus fuentes no responde: sigue con lo que tiene y avisa.
Qué tiene que hacer el programa
- Si la agenda no es JSON o no tiene la lista
@graph:Error: la agenda no es váliday nada más. Si la respuesta del tiempo no es JSON:Aviso: el servicio del tiempo ha devuelto una respuesta que no es JSON; la agenda va sin previsión; si trae"error": true:Aviso: el servicio del tiempo no responde (motivo); la agenda va sin previsión(el motivo está enreason). En los dos casos se sigue sin tiempo. - Un evento sin
dtstartválido (formatoAAAA-MM-DD HH:MM:SSy fecha que existe) se salta conAviso: el evento ID no tiene una fecha válida; sin título (o solo espacios), conAviso: el evento ID no tiene título. Los avisos salen en el orden de la agenda, antes que los días. - Los eventos se ordenan por fecha y hora (y por título si coinciden) y se agrupan por día. Cada día:
Día D de mes:(el día de la semana con mayúscula inicial) y la previsiónDescripción, MÍN–MÁX °C, lluvia P %(temperaturas redondeadas; descripción de la tabla ocódigo Nsi no está) osin previsión del tiemposi no hay datos de esa fecha. - Cada evento: dos espacios,
HH:MM título,(lugar)si lo tiene,·y el precio (gratissifreees 1; si no,pricecon dos decimales, coma y€, oprecio sin confirmarsi no hay o no es un número), y si es al aire libre· al aire librey, si la probabilidad de lluvia del día es del 50 % o más,· ¡Ojo! P % de probabilidad de lluvia. - Al final:
N actividades en D días; R al aire libre con riesgo de lluvia(1 actividad,1 día).
Entrada
La respuesta JSON de la agenda (puede ocupar varias líneas).
Una línea ---.
La respuesta JSON del servicio del tiempo.
Datos de referencia
| Código | Descripción |
|---|---|
| 0 | Despejado |
| 1, 2 | Poco nuboso |
| 3 | Nublado |
| 45, 48 | Niebla |
| 51, 53, 55 | Llovizna |
| 61 | Lluvia ligera |
| 63 | Lluvia |
| 65 | Lluvia fuerte |
| 80, 81 | Chubascos |
| 82 | Chubascos fuertes |
| 95 | Tormenta |
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Tres días de actividades
Entrada
{
"@context": "https://www.example.org/agenda",
"@graph": [
{
"id": "e1",
"title": "Teatro: La vida es sueño",
"dtstart": "2026-10-09 20:00:00",
"event-location": "Teatro del Barrio",
"free": 0,
"price": "12"
},
{
"id": "e2",
"title": "Concierto de jazz en el parque",
"dtstart": "2026-10-08 19:30:00",
"event-location": "Parque del Oeste",
"free": 1,
"aire-libre": 1
},
{
"id": "e3",
"title": "Ruta en bici junto al río",
"dtstart": "2026-10-09 18:00:00",
"event-location": "Madrid Río",
"free": 0,
"price": "5",
"aire-libre": 1
},
{
"id": "e4",
"title": "Taller de robótica para familias",
"dtstart": "2026-10-10 11:00:00",
"event-location": "Biblioteca municipal",
"free": 1
},
{
"id": "e5",
"title": "Mercadillo de segunda mano",
"dtstart": "2026-10-10 10:00:00",
"event-location": "Plaza Mayor",
"free": 1,
"aire-libre": 1
}
]
}
---
{"latitude": 40.42, "longitude": -3.7, "timezone": "Europe/Madrid", "daily_units": {"temperature_2m_max": "°C", "precipitation_probability_max": "%"}, "daily": {"time": ["2026-10-08", "2026-10-09", "2026-10-10"], "weathercode": [0, 61, 3], "temperature_2m_max": [24.1, 17.4, 19.6], "temperature_2m_min": [12.2, 10.5, 11.0], "precipitation_probability_max": [0, 80, 35]}}Salida por consola
Jueves 8 de octubre: Despejado, 12–24 °C, lluvia 0 % 19:30 Concierto de jazz en el parque (Parque del Oeste) · gratis · al aire libre Viernes 9 de octubre: Lluvia ligera, 11–17 °C, lluvia 80 % 18:00 Ruta en bici junto al río (Madrid Río) · 5,00 € · al aire libre · ¡Ojo! 80 % de probabilidad de lluvia 20:00 Teatro: La vida es sueño (Teatro del Barrio) · 12,00 € Sábado 10 de octubre: Nublado, 11–20 °C, lluvia 35 % 10:00 Mercadillo de segunda mano (Plaza Mayor) · gratis · al aire libre 11:00 Taller de robótica para familias (Biblioteca municipal) · gratis 5 actividades en 3 días; 1 al aire libre con riesgo de lluvia
Datos incompletos y el servicio del tiempo caído
Entrada
{
"@context": "https://www.example.org/agenda",
"@graph": [
{
"id": "e1",
"title": "Cine de verano",
"dtstart": "2026-10-11 21:30:00",
"event-location": "Terraza del centro cultural",
"free": 0,
"price": "3,50",
"aire-libre": 1
},
{
"id": "e2",
"title": "Exposición de fotografía",
"dtstart": "2026-10-12 10:00:00",
"free": 1
},
{
"id": "e3",
"title": "Carrera solidaria",
"dtstart": "2026-10-11 09:00:00",
"event-location": "Casa de Campo",
"free": 0,
"price": "15.5",
"aire-libre": 1
},
{
"id": "e4",
"title": "",
"dtstart": "2026-10-11 12:00:00",
"free": 0
},
{
"id": "e5",
"title": "Fiesta sin fecha",
"dtstart": "2026-13-01 12:00:00",
"free": 0
},
{
"id": "e6",
"title": "Recital de poesía",
"dtstart": "2026-10-11 19:00:00",
"free": 0
}
]
}
---
{"error": true, "reason": "Too many requests"}Salida por consola
Aviso: el servicio del tiempo no responde (Too many requests); la agenda va sin previsión Aviso: el evento e4 no tiene título Aviso: el evento e5 no tiene una fecha válida Domingo 11 de octubre: sin previsión del tiempo 09:00 Carrera solidaria (Casa de Campo) · 15,50 € · al aire libre 19:00 Recital de poesía · precio sin confirmar 21:30 Cine de verano (Terraza del centro cultural) · precio sin confirmar · al aire libre Lunes 12 de octubre: sin previsión del tiempo 10:00 Exposición de fotografía · gratis 4 actividades en 2 días; 0 al aire libre con riesgo de lluvia
Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. Desconfía de cada respuesta
json_decode($texto, true) devuelve null si el texto no es JSON. Comprueba también que está lo que necesitas (@graph, daily) antes de usarlo: un servicio puede cambiar o fallar.
$datos = json_decode($texto, true);
if (!is_array($datos)) return "... no es JSON";
if (!empty($datos["error"])) return "... no responde (" . $datos["reason"] . ")";2. De listas paralelas a un mapa
Open-Meteo devuelve una lista de fechas y, en la misma posición de otras listas, sus datos. Conviértelo en un array indexado por fecha y cruzar con la agenda será un simple $tiempo[$fecha] ?? null.
3. Fechas de verdad
DateTime::createFromFormat("Y-m-d H:i:s", $texto) lee la fecha; para descartar fechas imposibles (mes 13), compara el resultado formateado con el texto original. format("N") da el día de la semana (1 = lunes).
4. Ordenar por varios criterios
usort con [$a["dtstart"], $a["title"]] <=> [$b["dtstart"], $b["title"]] compara primero la fecha y, si coincide, el título.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.
Ejemplo
{
"@context": "https://www.example.org/agenda",
"@graph": [
{
"id": "e1",
"title": "Teatro: La vida es sueño",
"dtstart": "2026-10-09 20:00:00",
"event-location": "Teatro del Barrio",
"free": 0,
"price": "12"
},
{
"id": "e2",
"title": "Concierto de jazz en el parque",
"dtstart": "2026-10-08 19:30:00",
"event-location": "Parque del Oeste",
"free": 1,
"aire-libre": 1
},
{
"id": "e3",
"title": "Ruta en bici junto al río",
"dtstart": "2026-10-09 18:00:00",
"event-location": "Madrid Río",
"free": 0,
"price": "5",
"aire-libre": 1
},
{
"id": "e4",
"title": "Taller de robótica para familias",
"dtstart": "2026-10-10 11:00:00",
"event-location": "Biblioteca municipal",
"free": 1
},
{
"id": "e5",
"title": "Mercadillo de segunda mano",
"dtstart": "2026-10-10 10:00:00",
"event-location": "Plaza Mayor",
"free": 1,
"aire-libre": 1
}
]
}
---
{"latitude": 40.42, "longitude": -3.7, "timezone": "Europe/Madrid", "daily_units": {"temperature_2m_max": "°C", "precipitation_probability_max": "%"}, "daily": {"time": ["2026-10-08", "2026-10-09", "2026-10-10"], "weathercode": [0, 61, 3], "temperature_2m_max": [24.1, 17.4, 19.6], "temperature_2m_min": [12.2, 10.5, 11.0], "precipitation_probability_max": [0, 80, 35]}}
Jueves 8 de octubre: Despejado, 12–24 °C, lluvia 0 % 19:30 Concierto de jazz en el parque (Parque del Oeste) · gratis · al aire libre Viernes 9 de octubre: Lluvia ligera, 11–17 °C, lluvia 80 % 18:00 Ruta en bici junto al río (Madrid Río) · 5,00 € · al aire libre · ¡Ojo! 80 % de probabilidad de lluvia 20:00 Teatro: La vida es sueño (Teatro del Barrio) · 12,00 € Sábado 10 de octubre: Nublado, 11–20 °C, lluvia 35 % 10:00 Mercadillo de segunda mano (Plaza Mayor) · gratis · al aire libre 11:00 Taller de robótica para familias (Biblioteca municipal) · gratis 5 actividades en 3 días; 1 al aire libre con riesgo de lluvia
Solución explicada
Ver la solución completa
1<?php
2const DIAS = [1 => "lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"];
3const MESES = [1 => "enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"];
4// Códigos del tiempo de la OMM (los que usa Open-Meteo)
5const TIEMPO = [
6 0 => "Despejado", 1 => "Poco nuboso", 2 => "Poco nuboso", 3 => "Nublado", 45 => "Niebla", 48 => "Niebla",
7 51 => "Llovizna", 53 => "Llovizna", 55 => "Llovizna", 61 => "Lluvia ligera", 63 => "Lluvia", 65 => "Lluvia fuerte",
8 80 => "Chubascos", 81 => "Chubascos", 82 => "Chubascos fuertes", 95 => "Tormenta",
9];
10const RIESGO_LLUVIA = 50; // % a partir del cual se avisa en las actividades al aire libre
11
12[$textoAgenda, $textoTiempo] = array_pad(explode("\n---\n", str_replace("\r", "", stream_get_contents(STDIN)), 2), 2, "");
13
14/** El tiempo por fecha: ["2026-10-08" => ["codigo" => 0, "max" => 24.1, ...]], o un aviso si el servicio falla. */
15function leerTiempo(string $texto): array|string
16{
17 $datos = json_decode($texto, true);
18 if (!is_array($datos)) return "el servicio del tiempo ha devuelto una respuesta que no es JSON";
19 if (!empty($datos["error"])) return "el servicio del tiempo no responde (" . ($datos["reason"] ?? "sin motivo") . ")";
20 $d = $datos["daily"] ?? [];
21 $porFecha = [];
22 foreach ($d["time"] ?? [] as $k => $fecha) {
23 $porFecha[$fecha] = [
24 "codigo" => $d["weathercode"][$k] ?? null,
25 "max" => $d["temperature_2m_max"][$k] ?? null,
26 "min" => $d["temperature_2m_min"][$k] ?? null,
27 "lluvia" => $d["precipitation_probability_max"][$k] ?? null,
28 ];
29 }
30 return $porFecha;
31}
32
33function precio(array $e): string
34{
35 if (!empty($e["free"])) return "gratis";
36 if (!isset($e["price"]) || !is_numeric($e["price"])) return "precio sin confirmar";
37 return number_format((float) $e["price"], 2, ",", ".") . " €";
38}
39
40$agenda = json_decode($textoAgenda, true);
41if (!is_array($agenda) || !isset($agenda["@graph"]) || !is_array($agenda["@graph"])) {
42 echo "Error: la agenda no es válida\n";
43 exit;
44}
45$tiempo = leerTiempo($textoTiempo);
46if (is_string($tiempo)) {
47 echo "Aviso: $tiempo; la agenda va sin previsión\n";
48 $tiempo = [];
49}
50
51$eventos = [];
52foreach ($agenda["@graph"] as $e) {
53 $id = $e["id"] ?? "?";
54 $cuando = DateTime::createFromFormat("Y-m-d H:i:s", $e["dtstart"] ?? "");
55 if ($cuando === false || $cuando->format("Y-m-d H:i:s") !== $e["dtstart"]) {
56 echo "Aviso: el evento $id no tiene una fecha válida\n";
57 continue;
58 }
59 if (trim($e["title"] ?? "") === "") {
60 echo "Aviso: el evento $id no tiene título\n";
61 continue;
62 }
63 $eventos[] = $e + ["cuando" => $cuando];
64}
65usort($eventos, fn($a, $b) => [$a["dtstart"], $a["title"]] <=> [$b["dtstart"], $b["title"]]);
66
67$porDia = [];
68foreach ($eventos as $e) $porDia[$e["cuando"]->format("Y-m-d")][] = $e;
69$riesgo = 0;
70foreach ($porDia as $fecha => $lista) {
71 $f = $lista[0]["cuando"];
72 $cabecera = ucfirst(DIAS[(int) $f->format("N")]) . " " . $f->format("j") . " de " . MESES[(int) $f->format("n")] . ": ";
73 $t = $tiempo[$fecha] ?? null;
74 if ($t === null || $t["codigo"] === null) {
75 $cabecera .= "sin previsión del tiempo";
76 } else {
77 $cabecera .= (TIEMPO[$t["codigo"]] ?? "código " . $t["codigo"]) . ", " . round($t["min"]) . "–" . round($t["max"]) . " °C, lluvia " . $t["lluvia"] . " %";
78 }
79 echo "$cabecera\n";
80 foreach ($lista as $e) {
81 $linea = " " . $e["cuando"]->format("H:i") . " " . trim($e["title"]);
82 if (!empty($e["event-location"])) $linea .= " (" . $e["event-location"] . ")";
83 $linea .= " · " . precio($e);
84 if (!empty($e["aire-libre"])) {
85 $linea .= " · al aire libre";
86 if ($t !== null && $t["lluvia"] !== null && $t["lluvia"] >= RIESGO_LLUVIA) {
87 $linea .= " · ¡Ojo! " . $t["lluvia"] . " % de probabilidad de lluvia";
88 $riesgo++;
89 }
90 }
91 echo "$linea\n";
92 }
93}
94$n = count($eventos);
95$d = count($porDia);
96echo "$n " . ($n === 1 ? "actividad" : "actividades") . " en $d " . ($d === 1 ? "día" : "días") . "; $riesgo al aire libre con riesgo de lluvia\n";El valor de un mashup está en el cruce: la agenda sola no sabe que va a llover, y la previsión sola no sabe qué hay programado. La clave para unirlos es un campo común, aquí la fecha.
La degradación elegante es la regla de oro con servicios de terceros: si el del tiempo falla (cuota agotada, caída, respuesta rara), la agenda se muestra igual con un aviso. Una aplicación que muestra un error en blanco porque falló un servicio secundario es un mal mashup.
En producción se añadirían una caché (la previsión no cambia cada segundo y las APIs limitan el número de peticiones), tiempos de espera cortos con cURL y, a veces, peticiones en paralelo.
Los portales de datos abiertos de muchos ayuntamientos publican sus agendas en JSON con formatos parecidos a este: son una buena fuente para proyectos de clase.
Para ir más allá
- Haz las peticiones de verdad con cURL a Open-Meteo, con un tiempo de espera de 3 segundos.
- Guarda la respuesta del tiempo en una caché en fichero durante una hora.
- Sugiere, para cada actividad al aire libre con riesgo de lluvia, otra cubierta del mismo día.