Курсы валют - бесплатно, без СМС

9 Jun 2019

Всем привет! Сегодня расскажу про получение курсов валют. Сама по себе задача не сложная и ничего необычного здесь нет. Самый простой способ получить актуальный курс валюты - использовать какой-нибудь сторонний сервис для этого. Например, Fixer.io. Обращаемся к нему по Api, получаем данные ничего необычного. Обычно в подобных сервисах есть бесплатный пакет, который позволяет использовать сервис в небольшом проекте. В данном случае это 1000 запросов в месяц.

Однако у такого подхода может быть несколько недостатков.

  • Еще одна зависимость в проекте
  • Обращение к стороннему сервису, возможны задержки и даже недостуность
  • Лимит на количество запросов. В случае fixer - 1000 запросов в месяц. Может нехватить
  • Есть какая-то специфичная задача, нужно собрать статистику за большой промежуток времени и т.д.

Можно попробовать получить курсы валют напрямую из официальных источников. Курсы валют публикуются на сайтах ECB, ЦБ РФ, The Bank of England и т.д. Соответственно, публикуется курс заданой валюты относительно других валют.

К примеру, на сайте ЦБ РФ публикуется курс рубля относительно Eur, Usd и т.д. На сайте ... - курс евро относительно доллара, норвежской кроны и т.д.

Данные публикуются в различных форматах, где-то это просто html таблица, где-то XML, где-то JSON, где-то CSV. Далее приведены ссылки на получение данных:

Для остальных валют мне не требовалось извлекать курсы. Их можно попробовать извлечь с соответствующих ресурсов. Либо, например, взять курс EUR относитлельно конкретной валюты и высчитать курс этой самой валюты относительно EUR.

Для реализации парсера имеет смысл использовать такую структуру: создаем Retriever и Mapper, для каждой валюты. Retriever непосредственно извлекает курсы валют с сайта. Mapper приводит к общему формату.

public function retrieve(): array
{
    $url = 'https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/usd.xml';
    $result = $this->curlHelper->sendRequest($url, Request::METHOD_GET);
    $xml = simplexml_load_string($result);
    $json = json_encode($xml);
    $array = json_decode($json, true);
    return $array;
}
public function map(array $data): array
{
    $results = [];
    $rows = $data['DataSet']['Series']['Obs'];
    foreach ($rows as $row) {
        $attributes = $row['@attributes'];
        $results[] = [
            'date' => $attributes['TIME_PERIOD'],
            'rate' => $attributes['OBS_VALUE'],
        ];
    }
    return $results;
}

Также используется Enum со списком валют и Factory, которая возвращает для каждой валюты Retriever и Mapper.

public function createMapper(string $currency): MapperInterface
{
    $className = 'App\\Currency\\'.$currency.'\\Mapper';
    if (!class_exists($className)) {
        throw new \Exception('Wrong currency: '.$currency);
    }
    return new $className();
}    
public function createRetriever(string $currency): RetrieverInterface
{
    $curlHelper = new CurlHelper();
    $className = 'App\\Currency\\'.$currency.'\\Retriever';
    if (!class_exists($className)) {
        throw new \Exception('Wrong currency: '.$currency);
    }
    return new $className($curlHelper);
}

Остается еще одна небольшая проблема. Как правило, публикуются курсы валют только для рабочих дней. Для выходных остаются пропуски. При этом логично, что если есть курс на пятницу, а потом сразу на понедельник, то в субботу и воскресенье курс будет пятничным. Соответственно, напишем код, который будет дополнять курсы валют для выходных и праздников.

public function addMissingDates(array $data, string $endDate): array
{
    $currentDate = $data[0]['date'];
    $currentRate = $data[0]['rate'];
 
    $dataMap = $this->getDataMap($data);
 
    $results = [];
    for (;;) {
        $results[] = [
            'date' => $currentDate,
            'rate' => $currentRate,
        ];
 
        if ($currentDate == $endDate) {
            break;
        }
 
        $currentDate = (new \DateTime($currentDate))->modify('+1 day')->format('Y-m-d');
 
        if (isset($dataMap[$currentDate])) {
            $currentRate = $dataMap[$currentDate];
        }
    }
 
    return $results;
}

В результате мы можем получать курсы валют самостоятельно, из официальных источников.

Напоследок подготовил небольшой пример, в котором реализованы основные идеи из статьи. Пример сделан с использованием Symfony 4. Код доступен на github. Для запуска нужно клонировать репозиторий установить зависимости и запустить одну команду:

php bin/console currencies:update-all-rates

Данные курсов валют сохраняются в директорию currencies_data.

На этом пока все. Спасибо за внимание!