patternphpMinor
Currency conversion class with caching
Viewed 0 times
conversionwithcachingclasscurrency
Problem
I've just made this class, which supports caching. I'd really appreciate any comments on how to make it better etc.
Code
```
cacheFolder = ($folder == 'dcf') ? dirname(__FILE__).'/convert/' : $folder;
if (is_writable($this->cacheFolder) && $cache == TRUE) {
$this->cachable = TRUE;
$this->cacheTimeout = $cacheTimeout;
}
}
/*
* Main function for converting
*
* Set $round to FALSE to return full amount
*/
public function convert($amount = 1, $from = 'GBP', $to = 'USD', $round = TRUE)
{
# Check if cache file exists and pull rate
$rate = $this->get_cache($from.$to);
if ($rate !== FALSE) {
$return = $rate * $amount;
}
else {
if (!$this->validate_currency($to, $from)) {
throw new Exception('Invalid currency code - must be exactly 3 letters');
}
$response = $this->fetch($amount, $from, $to);
if (isset($response['err'])) {
throw new Exception('Invalid input');
}
$return = $response['v'];
$this->new_cache($from.$to, $response['rate']);
}
return ($round) ? abs(round($return, 2)) : abs($return);
}
/*
* Fetches data from external API
*/
protected function fetch($amount, $from, $to)
{
$url = "http://rate-exchange.appspot.com/currency?q={$amount}&from={$from}&to={$to}";
$amount = (float) $amount;
if (in_array('curl', get_loaded_extensions())) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, "http://rate-exchange.appspot.com/currency?q={$amount}&from={$from}&to={$to}");
$response = json_decode(curl_exec($ch), true);
}
else {
$response = json_decode(file_get_contents($url), true);
Code
```
cacheFolder = ($folder == 'dcf') ? dirname(__FILE__).'/convert/' : $folder;
if (is_writable($this->cacheFolder) && $cache == TRUE) {
$this->cachable = TRUE;
$this->cacheTimeout = $cacheTimeout;
}
}
/*
* Main function for converting
*
* Set $round to FALSE to return full amount
*/
public function convert($amount = 1, $from = 'GBP', $to = 'USD', $round = TRUE)
{
# Check if cache file exists and pull rate
$rate = $this->get_cache($from.$to);
if ($rate !== FALSE) {
$return = $rate * $amount;
}
else {
if (!$this->validate_currency($to, $from)) {
throw new Exception('Invalid currency code - must be exactly 3 letters');
}
$response = $this->fetch($amount, $from, $to);
if (isset($response['err'])) {
throw new Exception('Invalid input');
}
$return = $response['v'];
$this->new_cache($from.$to, $response['rate']);
}
return ($round) ? abs(round($return, 2)) : abs($return);
}
/*
* Fetches data from external API
*/
protected function fetch($amount, $from, $to)
{
$url = "http://rate-exchange.appspot.com/currency?q={$amount}&from={$from}&to={$to}";
$amount = (float) $amount;
if (in_array('curl', get_loaded_extensions())) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, "http://rate-exchange.appspot.com/currency?q={$amount}&from={$from}&to={$to}");
$response = json_decode(curl_exec($ch), true);
}
else {
$response = json_decode(file_get_contents($url), true);
Solution
Your class does too much. Split it up into multiple pieces:
Although it does plenty of things, it has no answer to the problem of how to actually recognize which currency any amount is in. Adding two amounts might be valid, because you add GBP and GBP, but might actually be invalid because of GBP and USD. Your variable would only contain the integer or float value.
So split it up. First create a class that actually represents an amount of money in a specified currency. This can be as easy as making a class with two public values, $amount and $currency, but usually you do not want to allow write access to these, so the two values should go into the constructor, stored as private properties, and be accessible via get methods.
Adding or subtracting monetary values is a common task. How can we add two amounts of the same currency? Simple addition. Let's add a method for it. Note that I add the method to the Money_Currency class, which can be discussed. If I do not want to do this, I'd need an independent class that does all the math. If you have such a class, try this different approach. If not, continue following me...
So now we are able to do a simple math operation:
Outputs
Easy. And completely unrelated to your currency conversion so far, but it solves a problem you might have, unless you are only offering a web service that inputs amount and currencies and translates this to the other value.
What about currency conversion? What about adding two different currencies? Decorator pattern to the rescue!
You can build a decorator that implements the same interface, which wraps around a currency object and does the calculations for converting the currency.
Let's fix the interface stuff first:
Then the converter:
Now some test (continues with the objects from above):
Output:
(we started with the 50 USD object from above, the conversion rate is completely arbitrary).
What have we got now? We can add amounts of the same currency. We can convert an amount into a different currency. We can also add amounts in different currencies via wrapping them into a converter first. That's pretty much all currency stuff should do.
Now how do you get this nice converter?
- It converts currencies.
- It validates currency identifier.
- It fetches HTTP resources
- It caches fetched HTTP resources.
Although it does plenty of things, it has no answer to the problem of how to actually recognize which currency any amount is in. Adding two amounts might be valid, because you add GBP and GBP, but might actually be invalid because of GBP and USD. Your variable would only contain the integer or float value.
So split it up. First create a class that actually represents an amount of money in a specified currency. This can be as easy as making a class with two public values, $amount and $currency, but usually you do not want to allow write access to these, so the two values should go into the constructor, stored as private properties, and be accessible via get methods.
class Money_Currency
{
/**
* @var float
*/
private $_amount;
/**
* @var string
*/
private $_currency;
public function __construct($amount, $currency)
{
$this->_amount = $amount;
$this->_currency = $currency;
}
public function getAmount()
{
return $this->_amount;
}
public function getCurrency()
{
return $this->_currency;
}
}Adding or subtracting monetary values is a common task. How can we add two amounts of the same currency? Simple addition. Let's add a method for it. Note that I add the method to the Money_Currency class, which can be discussed. If I do not want to do this, I'd need an independent class that does all the math. If you have such a class, try this different approach. If not, continue following me...
public function addAmount(Money_Currency $money)
{
if ($this->_currency !== $money->getCurrency()) {
throw new InvalidArgumentException('Can only add money from the same currency');
}
$this->_amount += $money->getAmount();
}So now we are able to do a simple math operation:
$m1 = new Money_Currency(20, 'USD');
$m2 = new Money_Currency(30, 'USD');
$m1->addAmount($m2);
echo $m1->getAmount() . " " . $m1->getCurrency();Outputs
50 USDEasy. And completely unrelated to your currency conversion so far, but it solves a problem you might have, unless you are only offering a web service that inputs amount and currencies and translates this to the other value.
What about currency conversion? What about adding two different currencies? Decorator pattern to the rescue!
You can build a decorator that implements the same interface, which wraps around a currency object and does the calculations for converting the currency.
Let's fix the interface stuff first:
interface Money_Currency
{
public function getAmount();
public function getCurrency();
}
class Money_Currency_Value implements Money_Currency
{
// instead of Money_Currency class from aboveThen the converter:
class Money_Currency_Converter implements Money_Currency
{
/**
* @var Money_Currency
*/
private $_money;
/**
* @var float
*/
private $_conversionrate;
/**
* @var string
*/
private $_sourcecurrency;
/**
* @var string
*/
private $_targetcurrency;
public function __construct($conversionrate, $sourcecurrency, $targetcurrency)
{
$this->_conversionrate = $conversionrate;
$this->_sourcecurrency = $sourcecurrency;
$this->_targetcurrency = $targetcurrency;
}
public function setMoney(Money_Currency $money)
{
if ($this->_sourcecurrency !== $money->getCurrency()) {
throw new InvalidArgumentException('The money value is in an incorrect currency for this converter');
}
$this->_money = $money;
}
public function getAmount()
{
return $this->_money->getAmount() * $this->_conversionrate;
}
public function getCurrency()
{
return $this->_targetcurrency;
}
}Now some test (continues with the objects from above):
$m3 = new Money_Currency_Value(10, 'GBP');
$convertGbpToUsd = new Money_Currency_Converter(1.5, 'GBP', 'USD');
$convertGbpToUsd->setMoney($m3);
echo "Converted ". $m3->getAmount() ." ". $m3->getCurrency() . " into ". $convertGbpToUsd->getAmount() . " " . $convertGbpToUsd->getCurrency();
try {
$m1->addAmount($m3); // fails
} catch (Exception $e) {
}
$m1->addAmount($convertGbpToUsd);
echo $m1->getAmount() . " " . $m1->getCurrency();Output:
Converted 10 GBP into 15 USD
65 USD(we started with the 50 USD object from above, the conversion rate is completely arbitrary).
What have we got now? We can add amounts of the same currency. We can convert an amount into a different currency. We can also add amounts in different currencies via wrapping them into a converter first. That's pretty much all currency stuff should do.
Now how do you get this nice converter?
Code Snippets
class Money_Currency
{
/**
* @var float
*/
private $_amount;
/**
* @var string
*/
private $_currency;
public function __construct($amount, $currency)
{
$this->_amount = $amount;
$this->_currency = $currency;
}
public function getAmount()
{
return $this->_amount;
}
public function getCurrency()
{
return $this->_currency;
}
}public function addAmount(Money_Currency $money)
{
if ($this->_currency !== $money->getCurrency()) {
throw new InvalidArgumentException('Can only add money from the same currency');
}
$this->_amount += $money->getAmount();
}$m1 = new Money_Currency(20, 'USD');
$m2 = new Money_Currency(30, 'USD');
$m1->addAmount($m2);
echo $m1->getAmount() . " " . $m1->getCurrency();interface Money_Currency
{
public function getAmount();
public function getCurrency();
}
class Money_Currency_Value implements Money_Currency
{
// instead of Money_Currency class from aboveclass Money_Currency_Converter implements Money_Currency
{
/**
* @var Money_Currency
*/
private $_money;
/**
* @var float
*/
private $_conversionrate;
/**
* @var string
*/
private $_sourcecurrency;
/**
* @var string
*/
private $_targetcurrency;
public function __construct($conversionrate, $sourcecurrency, $targetcurrency)
{
$this->_conversionrate = $conversionrate;
$this->_sourcecurrency = $sourcecurrency;
$this->_targetcurrency = $targetcurrency;
}
public function setMoney(Money_Currency $money)
{
if ($this->_sourcecurrency !== $money->getCurrency()) {
throw new InvalidArgumentException('The money value is in an incorrect currency for this converter');
}
$this->_money = $money;
}
public function getAmount()
{
return $this->_money->getAmount() * $this->_conversionrate;
}
public function getCurrency()
{
return $this->_targetcurrency;
}
}Context
StackExchange Code Review Q#19338, answer score: 5
Revisions (0)
No revisions yet.