FEATURE Added Money class for managing monetary amounts with currencies through the Money design pattern. Uses the CompositeDBField interface to contain multiple database columns in a single DBField

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@76100 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2009-05-05 08:10:51 +00:00
parent a9e1d45381
commit 5e8e47ef77
4 changed files with 540 additions and 1 deletions

View File

@ -3,7 +3,7 @@
* Represents a decimal field containing a currency amount.
* Currency the currency class only supports single currencies.
*
* @todo Add localization support, see http://open.silverstripe.com/ticket/2931
* @deprecated 2.5 Use Money class
*
* @package sapphire
* @subpackage model

View File

@ -0,0 +1,262 @@
<?php
/**
* Partially based on Zend_Currency.
*
* @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: Currency.php 6137 2007-08-19 14:55:27Z shreef $
*/
set_include_path(get_include_path() . PATH_SEPARATOR . BASE_PATH . '/sapphire/thirdparty');
require_once 'Zend/Currency.php';
/**
* Implements the "Money" pattern.
*
* @see http://www.martinfowler.com/eaaCatalog/money.html
*
* @todo Support different ways of rounding
* @todo Equality operators
* @todo Addition, substraction and allocation of values
* @todo Model validation for $allowedCurrencies
*
* @package sapphire
* @subpackage model
*/
class Money extends DBField implements CompositeDBField {
/**
* @var string $getCurrency()
*/
protected $currency;
/**
* @var float $currencyAmount
*/
protected $amount;
/**
* @var boolean $isChanged
*/
protected $isChanged = false;
/**
* @var string $locale
*/
protected $locale = null;
/**
* @var Zend_Currency
*/
protected $currencyLib;
/**
* Limit the currencies
* @var array $allowedCurrencies
*/
protected $allowedCurrencies;
/**
* @param array
*/
static $composite_db = array(
"Currency" => "Varchar(3)",
"Amount" => "Decimal(14,2)",
);
function __construct($name = null) {
$this->currencyLib = new Zend_Currency(i18n::default_locale());
parent::__construct($name);
}
public function composite_db(){
return self::$composite_db;
}
function requireField() {
$composite_db = $this->composite_db();
foreach($composite_db as $name => $type){
DB::requireField($this->tableName, $this->name.$name, $type);
}
}
function writeToManipulation(&$manipulation) {
$manipulation['fields'][$this->name.'Currency'] = $this->prepValueForDB($this->getCurrency());
$manipulation['fields'][$this->name.'Amount'] = $this->getAmount();
}
function setValue($value,$record=null) {
//var_dump($value);
//var_dump($record);
if($record && isset($record[$this->name . 'Currency']) && isset($record[$this->name . 'Amount'])) {
if($record[$this->name . 'Currency'] && $record[$this->name . 'Amount']) {
$this->setCurrency($record[$this->name . 'Currency']);
$this->setAmount($record[$this->name . 'Amount']);
} else {
$this->value = $this->nullValue();
}
} elseif ($value instanceof Money) {
$this->setCurrency($value->getCurrency());
$this->setAmount($value->getAmount());
} else if (is_array($value)) {
if (array_key_exists('Currency', $value)) {
$this->setCurrency($value['Currency']);
$this->isChanged = true;
}
if (array_key_exists('Amount', $value)) {
$this->setAmount($value['Amount']);
$this->isChanged = true;
}
} else {
user_error('Invalid value in Money->setValue()', E_USER_ERROR);
}
}
/**
* @return string
*/
function Nice($options = array()) {
return $this->currencyLib->toCurrency($this->getAmount(), $options);
}
/**
* @return string
*/
function getCurrency() {
return $this->currency;
}
/**
* @param string
*/
function setCurrency($currency) {
$this->currency = $currency;
}
/**
* @todo Return casted Float DBField?
*
* @return float
*/
function getAmount() {
return $this->amount;
}
/**
* @param float $amount
*/
function setAmount($amount) {
$this->amount = (float)$amount;
}
/**
* @return boolean
*/
function hasValue() {
return ($this->getCurrency() && is_numeric($this->getAmount()));
}
function isChanged() {
return $this->isChanged;
}
/**
* @param string $locale
*/
function setLocale($locale) {
$this->locale = $locale;
$this->currencyLib->setLocale($locale);
}
/**
* @return string
*/
function getLocale() {
return ($this->locale) ? $this->locale : i18n::get_locale();
}
/**
* @return string
*/
function getSymbol($currency = null, $locale = null) {
if($locale === null) $locale = $this->getLocale();
if($currency === null) $currency = $this->getCurrency();
return $this->currencyLib->getSymbol($currency, $locale);
}
/**
* @return string
*/
function getShortName($currency = null, $locale = null) {
if($locale === null) $locale = $this->getLocale();
if($currency === null) $currency = $this->getCurrency();
return $this->currencyLib->getShortName($currency, $locale);
}
/**
* @return string
*/
function getName($currency = null, $locale = null) {
if($locale === null) $locale = $this->getLocale();
if($currency === null) $currency = $this->getCurrency();
return $this->currencyLib->getName($currency, $locale);
}
/**
* @param array $arr
*/
function setAllowedCurrencies($arr) {
$this->allowedCurrencies = $arr;
}
/**
* @return array
*/
function getAllowedCurrencies() {
return $this->allowedCurrencies;
}
/**
* @todo Implement this
*/
function toString() {
}
/**
* Returns a CompositeField instance used as a default
* for form scaffolding.
*
* Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}
*
* @param string $title Optional. Localized title of the generated instance
* @return FormField
*/
public function scaffoldFormField($title = null) {
$fieldAmount = new NumericField($this->name."Amount", 'Amount');
$allowedCurrencies = $this->getAllowedCurrencies();
if($allowedCurrencies) {
$fieldCurrency = new DropdownField(
$this->name."Currency",
'Currency',
array_combine($allowedCurrencies,$allowedCurrencies)
);
} else {
$fieldCurrency = new TextField($this->name."Currency", 'Currency');
}
$field = new FieldGroup(
$fieldAmount,
$fieldCurrency
);
return $field;
}
}
?>

273
tests/model/MoneyTest.php Normal file
View File

@ -0,0 +1,273 @@
<?php
/**
* Partially based on Zend_CurrencyTest.
*
* @copyright Copyright (c) 2006 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: CurrencyTest.php 14644 2009-04-04 18:59:08Z thomas $
*/
/**
* @package sapphire
* @subpackage tests
*/
class MoneyTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/model/MoneyTest.yml';
/**
* Write a Money object to the database, then re-read it to ensure it
* is re-read properly.
*/
function testGettingWrittenDataObject() {
$obj = new MoneyTest_DataObject();
$m = new Money();
$m->setAmount(987.65);
$m->setCurrency('USD');
$obj->MyMoney = $m;
$this->assertEquals("$987.65", $obj->MyMoney->Nice(),
"Money field not added to data object properly when read prior to first writing the record."
);
$objID = $obj->write();
$moneyTest = DataObject::get_by_id('MoneyTest_DataObject',$objID);
$this->assertTrue($moneyTest instanceof MoneyTest_DataObject);
$this->assertEquals('USD', $moneyTest->MyMoneyCurrency);
$this->assertEquals(987.65, $moneyTest->MyMoneyAmount);
$this->assertEquals("$987.65", $moneyTest->MyMoney->Nice(),
"Money field not added to data object properly when read."
);
}
public function testToCurrency() {
$USD = new Money();
$USD->setCurrency('USD');
$USD->setLocale('en_US');
$EGP = new Money();
$EGP->setCurrency('EGP');
$EGP->setLocale('ar_EG');
$USD->setAmount(53292.18);
$this->assertSame('$53,292.18', $USD->Nice());
$USD->setAmount(53292.18);
$this->assertSame('$٥٣,٢٩٢.١٨', $USD->Nice(array('script' => 'Arab' )));
$USD->setAmount(53292.18);
$this->assertSame('$ ٥٣.٢٩٢,١٨', $USD->Nice(array('script' => 'Arab', 'format' => 'de_AT')));
$USD->setAmount(53292.18);
$this->assertSame('$ 53.292,18', $USD->Nice(array('format' => 'de_AT')));
$EGP->setAmount(53292.18);
$this->assertSame('ج.م. 53٬292٫18', $EGP->Nice());
$EGP->setAmount(53292.18);
$this->assertSame('ج.م.‏ ٥٣٬٢٩٢٫١٨', $EGP->Nice(array('script' => 'Arab' )));
$EGP->setAmount(53292.18);
$this->assertSame('ج.م.‏ ٥٣.٢٩٢,١٨', $EGP->Nice(array('script' =>'Arab', 'format' => 'de_AT')));
$EGP->setAmount(53292.18);
$this->assertSame('ج.م. 53.292,18', $EGP->Nice(array('format' => 'de_AT')));
$USD = new Money();
$USD->setLocale('en_US');
$USD->setAmount(53292.18);
$this->assertSame('$53,292.18', $USD->Nice());
/*
try {
$this->assertSame('$ 53,292.18', $USD->Nice('nocontent'));
$this->fail("No currency expected");
} catch (Exception $e) {
$this->assertContains("has to be numeric", $e->getMessage());
}
*/
/*
$INR = new Money();
$INR->setLocale('de_AT');
$INR->setCurrency('INR');
$INR->setAmount(1.2);
$this->assertSame('Rs. 1,20', $INR->Nice());
$INR->setAmount(1);
$this->assertSame('Re. 1,00', $INR->Nice());
$INR->setAmount(0);
$this->assertSame('Rs. 0,00', $INR->Nice());
$INR->setAmount(-3);
$this->assertSame('-Rs. 3,00', $INR->Nice());
*/
}
public function testGetSign() {
$EGP = new Money();
$EGP->setValue(array(
'Currency' => 'EGP',
'Amount' => 3.44
));
$EGP->setLocale('ar_EG');
$this->assertSame('ج.م.', $EGP->getSymbol('EGP','ar_EG'));
$this->assertSame('€', $EGP->getSymbol('EUR','de_AT'));
$this->assertSame('ج.م.', $EGP->getSymbol(null, 'ar_EG'));
//$this->assertSame('€', $EGP->getSymbol(null, 'de_AT'));
$this->assertSame('ج.م.', $EGP->getSymbol());
try {
$EGP->getSymbol('EGP', 'de_XX');
$this->setExpectedException("Exception");
} catch(Exception $e) {
}
$EUR = new Money();
$EUR->setValue(array(
'Currency' => 'EUR',
'Amount' => 3.44
));
$EUR->setLocale('de_DE');
$this->assertSame('€', $EUR->getSymbol());
}
public function testGetName()
{
$m = new Money();
$m->setValue(array(
'Currency' => 'EUR',
'Amount' => 3.44
));
$m->setLocale('ar_EG');
$this->assertSame('جنيه مصرى', $m->getName('EGP','ar_EG'));
$this->assertSame('Estnische Krone', $m->getName('EEK','de_AT'));
//$this->assertSame('جنيه مصرى', $m->getName(null, 'ar_EG'));
//$this->assertSame('Euro', $m->getName('de_AT'));
$this->assertSame('يورو', $m->getName());
try {
$m->getName('EGP', 'xy_XY');
$this->setExpectedException("Exception");
} catch(Exception $e) {
}
}
public function testGetShortName() {
$m = new Money();
$m->setValue(array(
'Currency' => 'EUR',
'Amount' => 3.44
));
$m->setLocale('de_AT');
$this->assertSame('EUR', $m->getShortName('Euro', 'de_AT'));
$this->assertSame('USD', $m->getShortName('US-Dollar','de_AT'));
//$this->assertSame('EUR', $m->getShortName(null, 'de_AT'));
$this->assertSame('EUR', $m->getShortName());
try {
$m->getShortName('EUR', 'xy_ZT');
$this->setExpectedException("Exception");
} catch(Exception $e) {
}
}
function testSetValueAsArray() {
$m = new Money();
$m->setValue(array(
'Currency' => 'EUR',
'Amount' => 3.44
));
$this->assertEquals(
$m->getCurrency(),
'EUR'
);
$this->assertEquals(
$m->getAmount(),
3.44
);
}
function testSetValueAsMoney() {
$m1 = new Money();
$m1->setValue(array(
'Currency' => 'EUR',
'Amount' => 3.44
));
$m2 = new Money();
$m2->setValue($m1);
$this->assertEquals(
$m2->getCurrency(),
'EUR'
);
$this->assertEquals(
$m2->getAmount(),
3.44
);
}
function testHasValue() {
$m1 = new Money();
$this->assertFalse($m1->hasValue());
$m2 = new Money();
$m2->setValue(array(
'Currency' => 'EUR',
'Amount' => 3.44
));
$this->assertTrue($m2->hasValue());
$m3 = new Money();
$m3->setValue(array(
'Currency' => 'EUR',
'Amount' => 0
));
$this->assertTrue($m3->hasValue());
}
function testLoadIntoDataObject() {
$obj = new MoneyTest_DataObject();
$this->assertType('Money', $obj->obj('MyMoney'));
$m = new Money();
$m->setValue(array(
'Currency' => 'EUR',
'Amount' => 1.23
));
$obj->MyMoney = $m;
$this->assertEquals($obj->MyMoney->getCurrency(), 'EUR');
$this->assertEquals($obj->MyMoney->getAmount(), 1.23);
}
function testWriteToDataObject() {
$obj = new MoneyTest_DataObject();
$m = new Money();
$m->setValue(array(
'Currency' => 'EUR',
'Amount' => 1.23
));
$obj->MyMoney = $m;
$obj->write();
$this->assertEquals(
'EUR',
DB::query(sprintf(
'SELECT "MyMoneyCurrency" FROM "MoneyTest_DataObject" WHERE "ID" = %d',
$obj->ID
))->value()
);
$this->assertEquals(
'1.23',
DB::query(sprintf(
'SELECT "MyMoneyAmount" FROM "MoneyTest_DataObject" WHERE "ID" = %d',
$obj->ID
))->value()
);
}
}
class MoneyTest_DataObject extends DataObject implements TestOnly {
static $db = array(
'MyMoney' => 'Money',
//'MyOtherMoney' => 'Money',
);
}
?>

View File

@ -0,0 +1,4 @@
MoneyTest_DataObject:
test1:
MyMoneyCurrency: EUR
MyMoneyAmount: 1.23