206 lines
5.4 KiB
PHP
206 lines
5.4 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Devizove kurzy CNB s file cache per datum.
|
|
*
|
|
* Pouziti:
|
|
* $cnb = CnbRates::getInstance();
|
|
* $czk = $cnb->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<string, array<string, array{rate: float, amount: int}>>
|
|
*/
|
|
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<int, array{amount: float, currency: string, date?: string}> $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<string, array{rate: float, amount: int}>
|
|
*/
|
|
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<string, array{rate: float, amount: int}>
|
|
*/
|
|
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;
|
|
}
|
|
}
|