Skip to content

Commit

Permalink
Rewrite cache and exchange rate tables functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ksdev-pl committed Jul 5, 2015
1 parent 86968c9 commit 6578651
Show file tree
Hide file tree
Showing 9 changed files with 1,028 additions and 347 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ $ composer require ksdev/nbp-currency-converter
## Usage

``` php
$converter = new Ksdev\NBPCurrencyConverter(
new GuzzleHttp\Client(),
'path/to/cache/folder'
use Ksdev\NBPCurrencyConverter\CurrencyConverter;
use Ksdev\NBPCurrencyConverter\ExRatesDayTableFinder;
use Ksdev\NBPCurrencyConverter\ExRatesDayTableFactory;
use GuzzleHttp\Client;

$converter = new CurrencyConverter(
new ExRatesDayTableFinder(
new Client(),
new ExRatesDayTableFactory(),
'path/to/cache/folder'
)
);
try {
$result = $converter->convert('123.4567', 'PLN', 'USD')
Expand Down
71 changes: 71 additions & 0 deletions src/CurrencyConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Ksdev\NBPCurrencyConverter;

class CurrencyConverter
{
private $ratesTableFinder;

public function __construct(ExRatesDayTableFinder $ratesTableFinder)
{
$this->ratesTableFinder = $ratesTableFinder;
}

/**
* Get the average exchange rates
*
* @param \DateTime $pubDate Optional rates table publication date
*
* @return array
*
* @throws \Exception
*/
public function averageExchangeRates(\DateTime $pubDate = null)
{
$file = $this->ratesTableFinder->getExRatesDayTable($pubDate);
return $file->parsedContent;
}

/**
* Convert amount from one currency to another
*
* @param string $fromAmount Amount with four digits after decimal point, e.g. '123.0000'
* @param string $fromCurrency E.g. 'USD' or 'EUR'
* @param string $toCurrency E.g. 'USD' or 'EUR'
* @param \DateTime $pubDate Optional rates table publication date
*
* @return array
*
* @throws \Exception
*/
public function convert($fromAmount, $fromCurrency, $toCurrency, \DateTime $pubDate = null)
{
if (!preg_match('/^\d+\.(\d{4})$/', $fromAmount)) {
throw new \Exception('Invalid format of amount');
}

$rates = $this->averageExchangeRates($pubDate);

$fromCurrency = strtoupper($fromCurrency);
$toCurrency = strtoupper($toCurrency);
if (!isset($rates['waluty'][$fromCurrency]) || !isset($rates['waluty'][$toCurrency])) {
throw new \Exception('Invalid currency code');
}

$fromMultiplier = str_replace(',', '.', $rates['waluty'][$fromCurrency]['przelicznik']);
$fromAverageRate = str_replace(',', '.', $rates['waluty'][$fromCurrency]['kurs_sredni']);
$toMultiplier = str_replace(',', '.', $rates['waluty'][$toCurrency]['przelicznik']);
$toAverageRate = str_replace(',', '.', $rates['waluty'][$toCurrency]['kurs_sredni']);

bcscale(6);
$plnAmount = bcdiv(bcmul($fromAmount, $fromAverageRate), $fromMultiplier);
$resultAmount = bcdiv(bcmul($plnAmount, $toMultiplier), $toAverageRate);
$roundedResult = BCMathHelper::bcround($resultAmount, 4);

return [
'publication_date' => $rates['data_publikacji'],
'amount' => $roundedResult,
'currency' => $toCurrency
];
}
}
61 changes: 61 additions & 0 deletions src/ExRatesDayTable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Ksdev\NBPCurrencyConverter;

class ExRatesDayTable
{
/** @var string */
public $rawContent;

/** @var array */
public $parsedContent;

/**
* @param string $rawContent Raw xml content
*
* @throws \Exception
*/
public function __construct($rawContent)
{
$this->rawContent = $rawContent;
$this->parsedContent = $this->parseXml($rawContent);
}

/**
* Transform the raw xml content into an array
*
* @param string $rawContent
*
* @return array
*
* @throws \Exception
*/
private function parseXml($rawContent)
{
$xml = new \SimpleXMLElement($rawContent);
if (empty($xml->numer_tabeli) || empty($xml->data_publikacji) || empty($xml->pozycja)) {
throw new \Exception('Invalid xml response content');
}
$rates = [
'numer_tabeli' => (string)$xml->numer_tabeli,
'data_publikacji' => (string)$xml->data_publikacji,
'waluty' => [
'PLN' => [
'nazwa_waluty' => 'złoty polski',
'przelicznik' => '1',
'kurs_sredni' => '1'
]
]
];
foreach ($xml->pozycja as $pozycja) {
$rates['waluty'] += [
(string)$pozycja->kod_waluty => [
'nazwa_waluty' => (string)$pozycja->nazwa_waluty,
'przelicznik' => (string)$pozycja->przelicznik,
'kurs_sredni' => (string)$pozycja->kurs_sredni
]
];
}
return $rates;
}
}
18 changes: 18 additions & 0 deletions src/ExRatesDayTableFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Ksdev\NBPCurrencyConverter;

class ExRatesDayTableFactory
{
/**
* Get new ExRatesDayTable
*
* @param string $rawContent Raw xml content
*
* @return ExRatesDayTable
*/
public function getInstance($rawContent)
{
return new ExRatesDayTable($rawContent);
}
}
217 changes: 217 additions & 0 deletions src/ExRatesDayTableFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

namespace Ksdev\NBPCurrencyConverter;

class ExRatesDayTableFinder
{
const NBP_XML_URL = 'http://www.nbp.pl/kursy/xml/';
const MAX_ONE_TIME_API_REQ = 7;

/** @var \GuzzleHttp\Client */
private $guzzle;

/** @var ExRatesDayTableFactory */
private $ratesTableFactory;

/** @var string */
private $cachePath;

/** @var \DateTime */
private $soughtPubDate;

/**
* @param \GuzzleHttp\Client $guzzle
* @param ExRatesDayTableFactory $ratesTableFactory
* @param string $cachePath Optional path to an existing folder where the cache files will be stored
*
* @throws \Exception
*/
public function __construct(
\GuzzleHttp\Client $guzzle,
ExRatesDayTableFactory $ratesTableFactory,
$cachePath = ''
) {
$this->guzzle = $guzzle;
$this->ratesTableFactory = $ratesTableFactory;
if ($cachePath) {
if (!is_dir($cachePath)) {
throw new \Exception('Invalid cache path');
}
$this->cachePath = rtrim((string)$cachePath, '/') . '/';
}
}

/**
* Get the ExRatesDayTable instance
*
* @param \DateTime $pubDate Optional rates table publication date
*
* @return ExRatesDayTable
*
* @throws \Exception
*/
public function getExRatesDayTable(\DateTime $pubDate = null)
{
$this->setSoughtPubDate($pubDate);

$i = 0;
do {
// Limit the number of times the loop repeats
if ($i === self::MAX_ONE_TIME_API_REQ) {
throw new \Exception('Max request to api limit has been reached');
}

// If user doesn't want a specific date, try to get the rates from the last working day
if (!$pubDate) {
$this->soughtPubDate = $this->soughtPubDate->sub(new \DateInterval('P1D'));
}

// Try to find the file in cache, otherwise download it
if ($this->cachePath && ($cachedXml = $this->getCachedXml())) {
$rawContent = $cachedXml;
} else {
$rawContent = $this->downloadXml();
}

// If a specific date is sought then break, otherwise continue
if ($pubDate) {
break;
}

$i++;
} while (!$rawContent);

if (!$rawContent) {
throw new \Exception('Exchange rates file not found');
}

return $this->ratesTableFactory->getInstance($rawContent);
}

/**
* Set the sought publication date necessary for finder operation
*
* @param \DateTime|null $pubDate
*
* @throws \Exception
*/
private function setSoughtPubDate($pubDate)
{
if ($pubDate instanceof \DateTime) {
if (!($pubDate >= new \DateTime('2002-01-02') && $pubDate <= new \DateTime())) {
throw new \Exception('Invalid publication date');
}
} else {
$pubDate = new \DateTime();
}
$this->soughtPubDate = $pubDate;
}

/**
* Get the raw xml content from a cache file
*
* @return string|int Content string or 0 if the file doesn't exist
*/
private function getCachedXml()
{
$filesArray = scandir($this->cachePath);
$filename = $this->matchFilename($filesArray);

if ($filename) {
$rawContent = file_get_contents($this->cachePath . $filename);
return $rawContent;
}

return 0;
}

/**
* Get the raw xml content from the NBP api
*
* @return string|int Content string or 0 if the file doesn't exist
*
* @throws \Exception
*/
private function downloadXml()
{
$filename = $this->findFileInRatesDir();
if ($filename) {
$response = $this->guzzle->get(self::NBP_XML_URL . $filename);
if ($response->getStatusCode() === 200) {
$rawContent = (string)$response->getBody();
if ($this->cachePath) {
file_put_contents($this->cachePath . $filename, $rawContent);
}
return $rawContent;
} else {
throw new \Exception(
"Invalid response status code: {$response->getStatusCode()} {$response->getReasonPhrase()}"
);
}
} else {
return 0;
}
}

/**
* Find the file related to the publication date
*
* @return string|int Filename or 0 if the file was not found
*
* @throws \Exception
*/
private function findFileInRatesDir()
{
$dirname = $this->constructDirname();

$response = $this->guzzle->get(self::NBP_XML_URL . $dirname);
if ($response->getStatusCode() === 200) {
$rawContent = (string)$response->getBody();
} else {
throw new \Exception(
"Invalid response status code: {$response->getStatusCode()} {$response->getReasonPhrase()}"
);
}

$filesArray = explode("\r\n", $rawContent);
$filename = $this->matchFilename($filesArray);

return $filename;
}

/**
* Construct the name of directory containing the files
*
* @return string
*/
private function constructDirname()
{
if ($this->soughtPubDate->format('Y') !== (new \DateTime())->format('Y')) {
$dirname = "dir{$this->soughtPubDate->format('Y')}.txt";
} else {
$dirname = 'dir.txt';
}
return $dirname;
}

/**
* Searches files array for a match to the publication date
*
* @todo Optimize to avoid unnecessary regex
*
* @param array $filesArray
*
* @return string|int Filename or 0 if the file was not found
*/
private function matchFilename(array $filesArray)
{
foreach ($filesArray as $filename) {
if (preg_match('/(a\d{3}z' . $this->soughtPubDate->format('ymd') . ')/', $filename, $matches)) {
$filename = "{$matches[1]}.xml";
return $filename;
}
}

return 0;
}
}
Loading

0 comments on commit 6578651

Please sign in to comment.