|null Разобранные курсы текущего запроса (в памяти процесса) */ protected ?array $rates = null; /** * @param array $config url (XML_daily.asp), timeout (сек, по умолчанию 10), * cache_path (файл кэша), ttl (сек, по умолчанию 86400) */ public function __construct(protected array $config) { } /** * Курс за 1 единицу валюты в рублях. * * @param string $code ISO 4217, например 'EUR' * @return float|null null — RUB (курс 1 не нужен, но метод его не отдаёт отдельно, см. ниже), * код не найден в ответе ЦБ, либо курс сейчас недоступен (сеть/парсинг) */ public function rate(string $code): ?float { $code = strtoupper($code); if ($code === 'RUB') { return 1.0; } return $this->rates()[$code] ?? null; } /** * @return array code => курс за 1 единицу в рублях; пустой массив при ошибке */ protected function rates(): array { if ($this->rates !== null) { return $this->rates; } $cache_path = $this->config['cache_path'] ?? (APPPATH . '/cache/currency_rate.json'); $ttl = (int)($this->config['ttl'] ?? 86400); if (is_file($cache_path) && (time() - filemtime($cache_path)) < $ttl) { $cached = json_decode((string)file_get_contents($cache_path), true); if (is_array($cached)) { return $this->rates = $cached; } } $rates = $this->fetch(); if ($rates !== null) { @file_put_contents($cache_path, json_encode($rates)); return $this->rates = $rates; } // Сеть недоступна — отдаём протухший кэш, если он вообще есть, чем ничего. if (is_file($cache_path)) { $cached = json_decode((string)file_get_contents($cache_path), true); if (is_array($cached)) { return $this->rates = $cached; } } return $this->rates = []; } /** * @return array|null null при любой ошибке сети/парсинга */ protected function fetch(): ?array { try { $response = (new Curl())->execute( HttpRequest::factory($this->config['url'] ?? 'https://www.cbr.ru/scripts/XML_daily.asp') ->method('GET') ->timeout((int)($this->config['timeout'] ?? 10)) ); } catch (RuntimeException) { return null; } if (!$response->isSuccess()) { return null; } // Ответ ЦБ — windows-1251, не UTF-8. $xml_body = @iconv('windows-1251', 'UTF-8//IGNORE', $response->body()); if ($xml_body === false) { return null; } try { $xml = new \SimpleXMLElement($xml_body); } catch (Throwable) { return null; } $rates = []; foreach ($xml->Valute as $valute) { $code = (string)$valute->CharCode; $nominal = (float)str_replace(',', '.', (string)$valute->Nominal); $value = (float)str_replace(',', '.', (string)$valute->Value); if ($code === '' || $nominal <= 0) { continue; } $rates[$code] = $value / $nominal; } return $rates; } }