fontdelbosc-eltemps/includes/class-fontdelbosc-data-manager.php
2026-10-12 11:55:23 +00:00

260 lines
7.7 KiB
PHP

<?php
/**
* Data manager. Orchestrates the API sources with per-view fallback chains
* and Transients API caching (stale-while-error pattern).
*
* @package FontdelboscEltemps
*/
defined( 'ABSPATH' ) || exit;
/**
* Orchestrates API sources, caching and fallback chains.
*/
final class Fontdelbosc_Data_Manager {
/**
* Warms or refreshes every cache, regardless of who triggers it.
* Hooked to the hourly fontdelbosc_refresh_weather WP-Cron event so
* visitors are never served by a cold cache.
*
* @return void
*/
public static function refresh(): void {
self::get_current();
self::get_hourly( 24 );
self::get_daily();
$now = new DateTimeImmutable( 'now', new DateTimeZone( FONTDELBOSC_ELTEMPS_TIMEZONE ) );
$previous = $now->modify( 'first day of previous month' );
self::get_historical( (int) $previous->format( 'Y' ), (int) $previous->format( 'n' ) );
}
/**
* Returns normalized current conditions.
*
* Chain: Meteocat XEMA station (when key and station are configured)
* → Open-Meteo current.
*
* @return array<string,mixed>|null
*/
public static function get_current(): ?array {
$options = Fontdelbosc_Plugin::get_options();
$data = self::cached(
'fontdelbosc_current',
$options['cache_ttl_current'],
static function () use ( $options ): ?array {
if ( '' !== $options['meteocat_api_key'] && '' !== $options['meteocat_station_code'] ) {
$current = Fontdelbosc_Api_Meteocat::get_current( $options['meteocat_api_key'], $options['meteocat_station_code'] );
if ( null !== $current ) {
$hourly = Fontdelbosc_Data_Manager::get_hourly( 1 );
$firsthour = Fontdelbosc_Payload::path( $hourly, 'hours', 0 );
if ( is_array( $firsthour ) && null !== Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $firsthour, 'condition' ) ) ) {
$current['condition'] = Fontdelbosc_Payload::get( $firsthour, 'condition' );
}
return $current;
}
}
return Fontdelbosc_Api_OpenMeteo::get_current();
}
);
if ( ! is_array( $data ) ) {
return null;
}
return array(
'temperature' => Fontdelbosc_Payload::get( $data, 'temperature' ),
'apparent' => Fontdelbosc_Payload::get( $data, 'apparent' ),
'humidity' => Fontdelbosc_Payload::get( $data, 'humidity' ),
'precipitation' => Fontdelbosc_Payload::get( $data, 'precipitation' ),
'wind_speed' => Fontdelbosc_Payload::get( $data, 'wind_speed' ),
'wind_direction' => Fontdelbosc_Payload::get( $data, 'wind_direction' ),
'condition' => Fontdelbosc_Payload::get( $data, 'condition' ),
'source' => Fontdelbosc_Payload::get( $data, 'source' ),
'time' => Fontdelbosc_Payload::get( $data, 'time' ),
);
}
/**
* Returns the hourly forecast sliced to the requested number of hours.
*
* Chain: Meteocat municipal → AEMET hourly → Open-Meteo hourly.
*
* @param int $hours Number of hours (1-48).
* @return array<string,mixed>|null
*/
public static function get_hourly( int $hours = 24 ): ?array {
$options = Fontdelbosc_Plugin::get_options();
$data = self::cached(
'fontdelbosc_forecast_hourly',
$options['cache_ttl_forecast'],
static function () use ( $options ): ?array {
if ( '' !== $options['meteocat_api_key'] ) {
$data = Fontdelbosc_Api_Meteocat::get_hourly( $options['meteocat_api_key'] );
if ( null !== $data ) {
return $data;
}
}
if ( '' !== $options['aemet_api_key'] ) {
$data = Fontdelbosc_Api_Aemet::get_hourly( $options['aemet_api_key'] );
if ( null !== $data ) {
return $data;
}
}
return Fontdelbosc_Api_OpenMeteo::get_hourly();
}
);
$entries = array_values( array_filter( Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'hours' ) ), 'is_array' ) );
$source = Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $data, 'source' ) );
if ( '' === $source || array() === $entries ) {
return null;
}
return array(
'source' => $source,
'hours' => array_slice( $entries, 0, max( 1, min( 48, $hours ) ) ),
);
}
/**
* Returns the daily forecast sliced to the requested number of days.
*
* Chain: AEMET daily → Open-Meteo daily.
*
* @param int $days Number of days (1-7).
* @return array<string,mixed>|null
*/
public static function get_daily( int $days = 7 ): ?array {
$options = Fontdelbosc_Plugin::get_options();
$data = self::cached(
'fontdelbosc_forecast_daily',
$options['cache_ttl_forecast'],
static function () use ( $options ): ?array {
if ( '' !== $options['aemet_api_key'] ) {
$data = Fontdelbosc_Api_Aemet::get_daily( $options['aemet_api_key'] );
if ( null !== $data ) {
return $data;
}
}
return Fontdelbosc_Api_OpenMeteo::get_daily();
}
);
$entries = array_values( array_filter( Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'days' ) ), 'is_array' ) );
$source = Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $data, 'source' ) );
if ( '' === $source || array() === $entries ) {
return null;
}
return array(
'source' => $source,
'days' => array_slice( $entries, 0, max( 1, min( 7, $days ) ) ),
);
}
/**
* Returns the daily history for a month (Open-Meteo ERA5 archive).
*
* @param int $year Year (1940 onwards).
* @param int $month Month (1-12).
* @return array<string,mixed>|null
*/
public static function get_historical( int $year, int $month ): ?array {
$year = max( 1940, min( (int) gmdate( 'Y' ), $year ) );
$month = max( 1, min( 12, $month ) );
$options = Fontdelbosc_Plugin::get_options();
$data = self::cached(
'fontdelbosc_historical_' . $year . '_' . $month,
$options['cache_ttl_historical'],
static function () use ( $year, $month ): ?array {
return Fontdelbosc_Api_OpenMeteo::get_historical( $year, $month );
}
);
$entries = array_values( array_filter( Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'days' ) ), 'is_array' ) );
$source = Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $data, 'source' ) );
if ( '' === $source || array() === $entries ) {
return null;
}
return array(
'source' => $source,
'year' => $year,
'month' => $month,
'days' => $entries,
'stats' => Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'stats' ) ),
);
}
/**
* Returns the license attribution line for a data source slug.
*
* @param string $source Source slug (meteocat, aemet, open-meteo).
* @return string
*/
public static function attribution( string $source ): string {
$map = array(
'meteocat' => '© Servei Meteorològic de Catalunya. Generalitat de Catalunya',
'aemet' => '© AEMET. Gobierno de España',
'open-meteo' => 'Weather data by Open-Meteo.com (CC BY 4.0)',
);
return $map[ $source ] ?? '';
}
/**
* Cache helper implementing stale-while-error. Fresh data is served
* until its TTL expires; on fetch failure the last cached payload is
* served for up to one extra day.
*
* @param string $key Transient key.
* @param int $ttl Time-to-live in seconds.
* @param callable $fetch Fetch callback returning array|null.
* @return mixed
*/
private static function cached( string $key, int $ttl, callable $fetch ): mixed {
$cache = get_transient( $key );
if ( is_array( $cache ) && (int) Fontdelbosc_Payload::int( Fontdelbosc_Payload::get( $cache, 'expires' ) ?? 0 ) > time() ) {
return Fontdelbosc_Payload::get( $cache, 'data' );
}
$data = $fetch();
if ( null !== $data ) {
set_transient(
$key,
array(
'expires' => time() + $ttl,
'data' => $data,
),
$ttl + DAY_IN_SECONDS
);
return $data;
}
if ( is_array( $cache ) ) {
return Fontdelbosc_Payload::get( $cache, 'data' );
}
return null;
}
}