v1.0.0
This commit is contained in:
commit
e1f5a47c96
22 changed files with 3155 additions and 0 deletions
407
includes/class-fontdelbosc-combustible-data-manager.php
Normal file
407
includes/class-fontdelbosc-combustible-data-manager.php
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
<?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
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue