-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite cache and exchange rate tables functionality
- Loading branch information
Showing
9 changed files
with
1,028 additions
and
347 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.