407 lines
12 KiB
PHP
407 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Orchestration: caching, filtering and sorting of station data.
|
|
*
|
|
* @package FontdelboscCombustible
|
|
*/
|
|
|
|
declare( strict_types=1 );
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* Caches API payloads and builds the final station selection.
|
|
*/
|
|
final class Fontdelbosc_Combustible_Data_Manager {
|
|
|
|
/**
|
|
* Transient holding the live province payload.
|
|
*/
|
|
public const PROVINCE_TRANSIENT = 'fontdelbosc_combustible_province_08';
|
|
|
|
/**
|
|
* Transient holding the stale-fallback copy of the province payload.
|
|
*/
|
|
public const STALE_TRANSIENT = 'fontdelbosc_combustible_province_08_stale';
|
|
|
|
/**
|
|
* Transient holding the product catalog.
|
|
*/
|
|
public const PRODUCTS_TRANSIENT = 'fontdelbosc_combustible_products';
|
|
|
|
/**
|
|
* Option holding fetch status for the admin screen.
|
|
*/
|
|
public const STATUS_OPTION = 'fontdelbosc_combustible_status';
|
|
|
|
/**
|
|
* Option holding plugin settings.
|
|
*/
|
|
public const OPTIONS_OPTION = 'fontdelbosc_combustible_options';
|
|
|
|
/**
|
|
* Stale copy lifetime: 25 hours.
|
|
*/
|
|
private const STALE_TTL = 90000;
|
|
|
|
/**
|
|
* Product catalog lifetime: 7 days.
|
|
*/
|
|
private const PRODUCTS_TTL = 604800;
|
|
|
|
/**
|
|
* Default settings.
|
|
*
|
|
* @return array<string,mixed>
|
|
*/
|
|
public static function defaults(): array {
|
|
return array(
|
|
'search_radius_km' => 15,
|
|
'max_stations' => 10,
|
|
'fuel_types' => Fontdelbosc_Combustible_Fuels::DEFAULT_IDS,
|
|
'show_only_public' => true,
|
|
'cache_ttl' => 3600,
|
|
'show_distance' => true,
|
|
'show_schedule' => true,
|
|
'sort_by' => 'distance',
|
|
'delete_data_on_uninstall' => false,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the merged settings.
|
|
*
|
|
* @return array<string,mixed>
|
|
*/
|
|
public static function options(): array {
|
|
$clean = array();
|
|
|
|
foreach ( Fontdelbosc_Combustible_Payload::arr( get_option( self::OPTIONS_OPTION, array() ) ) as $key => $value ) {
|
|
if ( is_string( $key ) ) {
|
|
$clean[ $key ] = $value;
|
|
}
|
|
}
|
|
|
|
return array_merge( self::defaults(), $clean );
|
|
}
|
|
|
|
/**
|
|
* Returns the cached province payload, refreshing it when stale.
|
|
*
|
|
* @param bool $force_refresh Skip the live cache.
|
|
* @return array{fecha:string,stations:array<int,array<mixed>>,fetched_at:int,duration:float,stale?:bool}|null
|
|
*/
|
|
public static function get_payload( bool $force_refresh = false ): ?array {
|
|
if ( ! $force_refresh ) {
|
|
$cached = get_transient( self::PROVINCE_TRANSIENT );
|
|
|
|
if ( is_array( $cached ) ) {
|
|
return self::shape( $cached );
|
|
}
|
|
}
|
|
|
|
$payload = Fontdelbosc_Combustible_Api::fetch_province();
|
|
|
|
if ( null === $payload ) {
|
|
$stale = get_transient( self::STALE_TRANSIENT );
|
|
|
|
if ( is_array( $stale ) ) {
|
|
$shaped = self::shape( $stale );
|
|
$shaped['stale'] = true;
|
|
|
|
return $shaped;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
set_transient( self::PROVINCE_TRANSIENT, $payload, max( 60, Fontdelbosc_Combustible_Payload::int( self::options()['cache_ttl'] ?? null ) ?? 3600 ) );
|
|
set_transient( self::STALE_TRANSIENT, $payload, self::STALE_TTL );
|
|
self::record_status( $payload );
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Returns the cached product catalog, possibly empty.
|
|
*
|
|
* @return array<int,array{id:int,name:string,abbr:string}>
|
|
*/
|
|
public static function get_products(): array {
|
|
$cached = get_transient( self::PRODUCTS_TRANSIENT );
|
|
|
|
if ( is_array( $cached ) ) {
|
|
return self::shape_products( $cached );
|
|
}
|
|
|
|
$products = Fontdelbosc_Combustible_Api::fetch_products();
|
|
|
|
if ( null === $products ) {
|
|
return array();
|
|
}
|
|
|
|
set_transient( self::PRODUCTS_TRANSIENT, $products, self::PRODUCTS_TTL );
|
|
|
|
return $products;
|
|
}
|
|
|
|
/**
|
|
* Builds the final station selection.
|
|
*
|
|
* @param array<string,mixed> $args Overrides: radius, max, fuel, sort, only_public.
|
|
* @return array{stations:array<int,array<mixed>>,fecha:string,stale:bool,radius:int,fuels:array<int>,fields:array<int,string>}
|
|
*/
|
|
public static function get_stations( array $args = array() ): array {
|
|
$options = self::options();
|
|
$opt_radius = Fontdelbosc_Combustible_Payload::int( $options['search_radius_km'] ?? null ) ?? 15;
|
|
$opt_max = Fontdelbosc_Combustible_Payload::int( $options['max_stations'] ?? null ) ?? 10;
|
|
$opt_sort = Fontdelbosc_Combustible_Payload::str( $options['sort_by'] ?? null ) ?? 'distance';
|
|
$opt_public = ! empty( $options['show_only_public'] );
|
|
$only_public = $opt_public && ( $args['only_public'] ?? true );
|
|
$radius = max( 5, min( 50, Fontdelbosc_Combustible_Payload::int( $args['radius'] ?? null ) ?? $opt_radius ) );
|
|
$max = max( 1, min( 20, Fontdelbosc_Combustible_Payload::int( $args['max'] ?? null ) ?? $opt_max ) );
|
|
$fuels = self::fuel_ids( Fontdelbosc_Combustible_Payload::str( $args['fuel'] ?? null ) ?? '', $options );
|
|
$sort_att = Fontdelbosc_Combustible_Payload::str( $args['sort'] ?? null );
|
|
$sort = ( $sort_att ?? $opt_sort ) === 'price_asc' ? 'price_asc' : 'distance';
|
|
|
|
$stations = array();
|
|
$fields = array();
|
|
$stale = false;
|
|
$fecha = '';
|
|
$payload = self::get_payload();
|
|
|
|
if ( null !== $payload ) {
|
|
$stale = ! empty( $payload['stale'] );
|
|
$fecha = $payload['fecha'];
|
|
$selected = self::select( $payload['stations'], $radius, $fuels, $sort, $only_public );
|
|
$stations = $selected['stations'];
|
|
$fields = $selected['fields'];
|
|
}
|
|
|
|
return array(
|
|
'stations' => array_slice( $stations, 0, $max ),
|
|
'fecha' => $fecha,
|
|
'stale' => $stale,
|
|
'radius' => $radius,
|
|
'fuels' => $fuels,
|
|
'fields' => $fields,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes every cache entry.
|
|
*/
|
|
public static function flush_cache(): void {
|
|
delete_transient( self::PROVINCE_TRANSIENT );
|
|
delete_transient( self::STALE_TRANSIENT );
|
|
delete_transient( self::PRODUCTS_TRANSIENT );
|
|
}
|
|
|
|
/**
|
|
* Filters, sorts and dedupes the normalized station list.
|
|
*
|
|
* @param array<int,array<mixed>> $stations Stations.
|
|
* @param int $radius Radius in km.
|
|
* @param array<int> $fuels Product IDs.
|
|
* @param string $sort distance|price_asc.
|
|
* @param bool $only_public Keep Tipo Venta "P" only.
|
|
* @return array{stations:array<int,array<mixed>>,fields:array<int,string>}
|
|
*/
|
|
private static function select( array $stations, int $radius, array $fuels, string $sort, bool $only_public ): array {
|
|
$labels = array();
|
|
|
|
foreach ( $stations as $station ) {
|
|
foreach ( Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $station, 'prices' ) ) as $label => $_unused ) {
|
|
if ( is_string( $label ) ) {
|
|
$labels[ $label ] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$fields = self::fuel_fields( $fuels, array_keys( $labels ) );
|
|
$keep = array();
|
|
|
|
foreach ( $stations as $station ) {
|
|
$tipo = Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $station, 'tipo_venta' ) ) ?? '';
|
|
|
|
if ( $only_public && 'P' !== $tipo ) {
|
|
continue;
|
|
}
|
|
|
|
$distance = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $station, 'distance' ) );
|
|
|
|
if ( null === $distance || $distance > (float) $radius ) {
|
|
continue;
|
|
}
|
|
|
|
$prices = array();
|
|
|
|
foreach ( $fields as $field ) {
|
|
$price = Fontdelbosc_Combustible_Payload::float(
|
|
Fontdelbosc_Combustible_Payload::get( Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $station, 'prices' ) ), $field )
|
|
);
|
|
|
|
if ( null !== $price ) {
|
|
$prices[ $field ] = $price;
|
|
}
|
|
}
|
|
|
|
if ( array() === $prices ) {
|
|
continue;
|
|
}
|
|
|
|
$station['selected_prices'] = $prices;
|
|
$station['best_price'] = min( $prices );
|
|
$keep[] = $station;
|
|
}
|
|
|
|
usort(
|
|
$keep,
|
|
static function ( array $a, array $b ) use ( $sort ): int {
|
|
$a_price = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $a, 'best_price' ) );
|
|
$b_price = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $b, 'best_price' ) );
|
|
$a_key = 'price_asc' === $sort ? ( $a_price ?? INF ) : Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $a, 'distance' ) ) ?? INF;
|
|
$b_key = 'price_asc' === $sort ? ( $b_price ?? INF ) : Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $b, 'distance' ) ) ?? INF;
|
|
|
|
return $a_key <=> $b_key;
|
|
}
|
|
);
|
|
|
|
return array(
|
|
'stations' => $keep,
|
|
'fields' => $fields,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolves the fuel selection from a shortcode attribute or the options.
|
|
*
|
|
* @param string $att Raw "1,4" style attribute.
|
|
* @param array<string,mixed> $options Settings.
|
|
* @return array<int>
|
|
*/
|
|
private static function fuel_ids( string $att, array $options ): array {
|
|
if ( '' !== $att ) {
|
|
$ids = array();
|
|
|
|
foreach ( explode( ',', $att ) as $piece ) {
|
|
$id = Fontdelbosc_Combustible_Payload::int( trim( $piece ) );
|
|
|
|
if ( null !== $id && $id > 0 ) {
|
|
$ids[] = $id;
|
|
}
|
|
}
|
|
|
|
if ( array() !== $ids ) {
|
|
return array_values( array_unique( $ids ) );
|
|
}
|
|
}
|
|
|
|
$saved = array();
|
|
|
|
foreach ( Fontdelbosc_Combustible_Payload::arr( $options['fuel_types'] ?? null ) as $id ) {
|
|
$int = Fontdelbosc_Combustible_Payload::int( $id );
|
|
|
|
if ( null !== $int && $int > 0 ) {
|
|
$saved[] = $int;
|
|
}
|
|
}
|
|
|
|
return array() === $saved ? Fontdelbosc_Combustible_Fuels::DEFAULT_IDS : array_values( array_unique( $saved ) );
|
|
}
|
|
|
|
/**
|
|
* Maps product IDs to station price fields using the catalog.
|
|
*
|
|
* @param array<int> $ids Product IDs.
|
|
* @param array<string> $labels Price field labels present in the payload.
|
|
* @return array<int,string>
|
|
*/
|
|
private static function fuel_fields( array $ids, array $labels ): array {
|
|
$names = array();
|
|
|
|
foreach ( self::get_products() as $product ) {
|
|
$names[ $product['id'] ] = $product['name'];
|
|
}
|
|
|
|
$fields = array();
|
|
|
|
foreach ( $ids as $id ) {
|
|
$name = $names[ (int) $id ] ?? null;
|
|
$field = null === $name ? null : Fontdelbosc_Combustible_Fuels::station_field( $name, $labels );
|
|
|
|
if ( null !== $field ) {
|
|
$fields[ (int) $id ] = $field;
|
|
}
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Coerces a cached payload back into its typed shape.
|
|
*
|
|
* @param array<mixed> $raw Cached value.
|
|
* @return array{fecha:string,stations:array<int,array<mixed>>,fetched_at:int,duration:float,stale?:bool}
|
|
*/
|
|
private static function shape( array $raw ): array {
|
|
$stations = array();
|
|
|
|
foreach ( Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $raw, 'stations' ) ) as $station ) {
|
|
if ( is_array( $station ) ) {
|
|
$stations[] = $station;
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'fecha' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'fecha' ) ) ?? '',
|
|
'stations' => $stations,
|
|
'fetched_at' => Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $raw, 'fetched_at' ) ) ?? time(),
|
|
'duration' => Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $raw, 'duration' ) ) ?? 0.0,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Coerces a cached product catalog back into its typed shape.
|
|
*
|
|
* @param array<mixed> $raw Cached value.
|
|
* @return array<int,array{id:int,name:string,abbr:string}>
|
|
*/
|
|
private static function shape_products( array $raw ): array {
|
|
$products = array();
|
|
|
|
foreach ( $raw as $entry ) {
|
|
$id = Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $entry, 'IDProducto' ) )
|
|
?? Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $entry, 'id' ) );
|
|
|
|
if ( null === $id ) {
|
|
continue;
|
|
}
|
|
|
|
$products[] = array(
|
|
'id' => $id,
|
|
'name' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $entry, 'name' ) ) ?? (string) $id,
|
|
'abbr' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $entry, 'abbr' ) ) ?? '',
|
|
);
|
|
}
|
|
|
|
return $products;
|
|
}
|
|
|
|
/**
|
|
* Persists fetch status for the admin screen.
|
|
*
|
|
* @param array<string,mixed> $payload Fresh payload.
|
|
*/
|
|
private static function record_status( array $payload ): void {
|
|
update_option(
|
|
self::STATUS_OPTION,
|
|
array(
|
|
'last_success' => Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $payload, 'fetched_at' ) ) ?? time(),
|
|
'last_duration' => Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $payload, 'duration' ) ) ?? 0.0,
|
|
'last_data_date' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $payload, 'fecha' ) ) ?? '',
|
|
),
|
|
false
|
|
);
|
|
}
|
|
}
|