toCzk(1000.0, 'EUR', '2026-03-01'); */ declare(strict_types=1); class CnbRates { private const API_URL = 'https://api.cnb.cz/cnbapi/exrates/daily'; private const CACHE_FILE = 'cnb_rates_cache.json'; // Kurzy starsi nez dnesek se nemeni, cachujem navzdy. // Dnesni kurz cachujeme na 6 hodin (muze se behem dne aktualizovat). private const TODAY_CACHE_TTL = 21600; /** * In-memory cache: date => currency => {rate, amount} * @var array> */ private array $ratesByDate = []; private static ?CnbRates $instance = null; public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->loadCache(); } /** * Prevede castku na CZK dle kurzu platneho k danemu datu. */ public function toCzk(float $amount, string $currency, string $date = ''): float { if ($currency === 'CZK') { return $amount; } $rates = $this->getRatesForDate($date ?: date('Y-m-d')); if (!isset($rates[$currency])) { return $amount; } $info = $rates[$currency]; return $amount * ($info['rate'] / $info['amount']); } /** * Secte pole [{amount, currency, date?}] do jedne CZK castky. * Kazda polozka muze mit vlastni datum pro kurz. * * @param array $items */ public function sumToCzk(array $items): float { $total = 0.0; foreach ($items as $item) { $total += $this->toCzk( (float) $item['amount'], (string) $item['currency'], (string) ($item['date'] ?? '') ); } return round($total, 2); } /** * @return array */ private function getRatesForDate(string $date): array { if (isset($this->ratesByDate[$date])) { return $this->ratesByDate[$date]; } $rates = $this->fetchFromApi($date); if ($rates !== []) { $this->ratesByDate[$date] = $rates; $this->saveCache(); } return $rates; } // --- Cache --- private function getCachePath(): string { return sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::CACHE_FILE; } private function loadCache(): void { $path = $this->getCachePath(); if (!file_exists($path)) { return; } $content = file_get_contents($path); if ($content === false) { return; } $data = json_decode($content, true); if (!is_array($data)) { return; } $today = date('Y-m-d'); foreach ($data as $date => $entry) { if (!is_array($entry) || !isset($entry['rates'], $entry['fetched_at'])) { continue; } // Dnesni kurz expiruje po TTL, starsi zustavaji navzdy if ($date === $today) { $age = time() - (int) $entry['fetched_at']; if ($age > self::TODAY_CACHE_TTL) { continue; } } $this->ratesByDate[$date] = $entry['rates']; } } private function saveCache(): void { $path = $this->getCachePath(); // Nacist existujici cache a mergovat $existing = []; if (file_exists($path)) { $content = file_get_contents($path); if ($content !== false) { $decoded = json_decode($content, true); if (is_array($decoded)) { $existing = $decoded; } } } $now = time(); foreach ($this->ratesByDate as $date => $rates) { // Neprepisuj existujici pokud uz tam je (zachovej fetched_at) if (!isset($existing[$date])) { $existing[$date] = [ 'rates' => $rates, 'fetched_at' => $now, ]; } } $json = json_encode($existing, JSON_THROW_ON_ERROR); file_put_contents($path, $json, LOCK_EX); } // --- API --- /** * @return array */ private function fetchFromApi(string $date): array { $url = self::API_URL . '?lang=EN&date=' . urlencode($date); $context = stream_context_create([ 'http' => ['timeout' => 5], ]); $response = @file_get_contents($url, false, $context); if ($response === false) { return []; } $data = json_decode($response, true); if (!is_array($data) || !isset($data['rates'])) { return []; } $rates = []; foreach ($data['rates'] as $entry) { if (!isset($entry['currencyCode'], $entry['rate'], $entry['amount'])) { continue; } $rates[$entry['currencyCode']] = [ 'rate' => (float) $entry['rate'], 'amount' => (int) $entry['amount'], ]; } return $rates; } }