Commit d55189be authored by Thorsten Buss's avatar Thorsten Buss
Browse files

* add currencyLookup interface

* add RubyMoney currencyLookupSupport (extende currencyList)
* add extended CurrencyList
* add default_currency
* add money::format
parent 7ffda0d4
Money
=====
[![Build Status](https://api.travis-ci.org/mathiasverraes/money.png?branch=master)](http://travis-ci.org/mathiasverraes/money)
PHP 5.3+ library to make working with money safer, easier, and fun!
> "If I had a dime for every time I've seen someone use FLOAT to store currency, I'd have $999.997634" -- [Bill Karwin](https://twitter.com/billkarwin/status/347561901460447232)
This is a fork of [Mathias Verraes' Money Library][4], extended with:
* add CurrencyLookup, for different currency sources (example for json-file included)
* extended List of currencies with settings (decimal_mark, subunit_factor, symbols, iso_code etc) from great [RubyMoney][5] (ISO 4217)
* add format method for formating the Money-string
In short: You shouldn't represent monetary values by a float. Wherever
you need to represent money, use this Money value object.
```php
<?php
......@@ -24,7 +23,7 @@ assert($part2->equals(Money::EUR(333)));
assert($part3->equals(Money::EUR(333)));
```
The documentation is available at http://money.readthedocs.org
The documentation (before the fork) is available at http://money.readthedocs.org
Installation
......@@ -35,9 +34,8 @@ Install the library using [composer][1]. Add the following to your `composer.jso
```json
{
"require": {
"mathiasverraes/money": "dev-master"
},
"minimum-stability": "dev"
"bnnet/bnmoney": "~1.0"
}
}
```
......@@ -50,8 +48,10 @@ $ composer.phar install
Integration
-----------
See [`MoneyBundle`][2] for [Symfony integration][3].
See [`MoneyBundle`][2] for [Symfony integration][3] (only before the fork).
[1]: http://getcomposer.org/
[2]: https://github.com/pink-tie/MoneyBundle/
[3]: http://symfony.com/
[4]: https://github.com/mathiasverraes/money
[5]: https://github.com/RubyMoney/money
\ No newline at end of file
{
"name": "mathiasverraes/money",
"description": "PHP implementation of Fowler's Money pattern",
"name": "bnet/bnmoney",
"description": "PHP Library for dealing with money and currency",
"type": "library",
"keywords": [ "Money", "Value Object", "Generic Sub-domain" ],
"homepage": "http://verraes.net/2011/04/fowler-money-pattern-in-php/",
"keywords": [ "Money", "Currency" ],
"homepage": "https://github.com/bussnet/money",
"license": "MIT",
"authors": [
{
"name": "Mathias Verraes",
"email": "mathias@verraes.net"
"name": "Thorsten Buss",
"email": "thorsten.buss@buss-networks.de"
}
],
"require": {
......@@ -27,12 +27,7 @@
"Money": "lib"
}
},
"config": {
"bin-dir": "bin"
},
"suggest": {
"pink-tie/money-bundle": "For Symfony integration."
},
"minimum-stability": "dev"
}
}
\ No newline at end of file
......@@ -13,34 +13,54 @@ namespace Money;
class Currency
{
/** @var string */
private $name;
private $iso_string;
/** @var array */
private static $currencies;
private static $currencies = array();
/** @var array */
private static $default_currency;
/** @var CurrencyLookup */
private static $currency_lookup;
/** @var bool flag if currency has extended options*/
private $is_extended_currency;
/**
* @param string $name
* @param string $iso_string
* @throws UnknownCurrencyException
*/
public function __construct($name)
public function __construct($iso_string)
{
if(!isset(static::$currencies)) {
static::$currencies = require __DIR__.'/currencies.php';
if (!array_key_exists($iso_string, static::$currencies)) {
// get currency from LookupHelper
if (static::$currency_lookup instanceof CurrencyLookup)
static::$currencies[$iso_string] = static::$currency_lookup->getCurrencyByIsoCode($iso_string);
elseif (empty(static::$currencies)) // if empty, load from original php-list
static::$currencies = require __DIR__ . '/currencies.php';
else
throw new UnknownCurrencyException($iso_string);
}
$this->iso_string = $iso_string;
$this->is_extended_currency = is_array(static::$currencies[$iso_string]);
if (!array_key_exists($name, static::$currencies)) {
throw new UnknownCurrencyException($name);
}
$this->name = $name;
// set currency data as obj data
if (is_array(static::$currencies[$iso_string])) {
foreach (static::$currencies[$iso_string] as $k => $v) {
$this->$k = $v;
}
}
}
/**
/**
* @return string
*/
public function getName()
public function getIsostring()
{
return $this->name;
return $this->iso_string;
}
/**
......@@ -49,7 +69,7 @@ class Currency
*/
public function equals(Currency $other)
{
return $this->name === $other->name;
return $this->iso_string === $other->iso_string;
}
/**
......@@ -57,6 +77,137 @@ class Currency
*/
public function __toString()
{
return $this->getName();
return $this->getIsostring();
}
/**
* @param array $default_currency
* @return $this
*/
public static function setDefaultCurrency($default_currency) {
self::$default_currency = $default_currency;
}
/**
* @return array
*/
public static function getDefaultCurrency() {
return self::$default_currency;
}
/**
* @param \Money\CurrencyLookup $currency_lookup
* @return $this
*/
public static function setCurrencyLookup(CurrencyLookup $currency_lookup) {
self::$currency_lookup = $currency_lookup;
}
/**
* @return \Money\CurrencyLookup
*/
public static function getCurrencyLookup() {
return self::$currency_lookup;
}
/**
* assert that this currency is extended
* @throws CurrencyIsNoExtendedCurrencyException
*/
protected function assertExtendedCurrency() {
if (!$this->is_extended_currency)
throw new CurrencyIsNoExtendedCurrencyException(sprintf('currency %s is no extended currency, which is neede for this method. Add CurrencyLookup', $this->getIsostring()));
}
/**
* return currency symbol or html_entity
* @param bool $asHtml
* @return mixed
*/
public function getSymbol($asHtml=false) {
return $asHtml
? $this->getHtmlEntity()
: $this->get('symbol');
}
/**
* return is_symbol_before amount
* @return bool
*/
public function getSymbolFirst() {
return !!$this->get('symbol_first');
}
/**
* return string|int if symbol are before or after the amount
* @param bool $asString
* @return int|string
*/
public function getSymbolPosition($asString=false) {
$position = $this->getSymbolFirst() ? -1 : 1;
return $asString
? ($position>0?'after':'before')
: $position;
}
/**
* return the decimal mark (splitter\sign) for currency
* @return string
*/
public function getDecimalMark() {
return $this->get('decimal_mark');
}
/**
* return the thousandsseparator (splitter|sign) for currency
* @return string
*/
public function getThousandsSeparator() {
return $this->get('thousands_separator');
}
/**
* return the amount of decimal places for this currency
* @return float|int
*/
public function getDecimalPlaces() {
if ($this->getSubunitToUnit() == 1)
return 0;
elseif ($this->getSubunitToUnit() % 10 == 0)
return floor(log10($this->getSubunitToUnit()));
else
return floor(log10($this->getSubunitToUnit()) + 1);
}
/**
* return the factor between unit/subunit
* @return int
*/
public function getSubunitToUnit() {
$this->assertExtendedCurrency();
return (int)$this->get('subunit_to_unit');
}
/**
* return the html_entity of the currency symbol
* @return string
*/
private function getHtmlEntity() {
return $this->get('html_entity');
}
/**
* return a extended currencyvalue if currency is extended and value exists
* @param $string
* @return mixed
*/
protected function get($key) {
$this->assertExtendedCurrency();
if (!isset($this->$key))
throw new UnknownCurrencyExtendedValueException(sprintf('the extended value %s is not set in currency %s', $key, $this->getIsostring()));
return $this->$key;
}
}
// set the defaultCurrency
Currency::setDefaultCurrency(@constant('DEFAULT_CURRENCY') ?: 'EUR');
\ No newline at end of file
<?php
/**
* This file is part of the Money library
*
* Copyright (c) 2013 Thorsten Buss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Money;
class CurrencyIsNoExtendedCurrencyException extends \Exception implements Exception { }
<?php
/**
* User: thorsten
* Date: 05.08.13
* Time: 16:57
*/
namespace Money;
interface CurrencyLookup {
/**
* Return an Array with currencySettings
* @param $iso_code
* @return array
*/
public function getCurrencyByIsoCode($iso_code);
}
<?php
/**
* User: thorsten
* Date: 06.08.13
* Time: 11:01
*/
namespace Money;
/**
* Class CurrencyLookupRubyMoney
* download a list of currencies with extended values and make it avialibe via CurrencyLookup Interface
* @package Money
*/
class CurrencyLookupRubyMoney implements CurrencyLookup {
const CURRENCY_FILE = 'https://raw.github.com/RubyMoney/money/master/config/currency_iso.json';
/**
* @var array
*/
public $currencies;
/**
* Load currency-json file and parse
* @param null $file
* @param string $cache_dir
*/
function __construct($file=null, $cache_dir='/tmp/') {
$file = $file ? : self::CURRENCY_FILE;
// read from cache
if (file_exists($cache_dir.'/'.basename($file)))
$file = $cache_dir . '/' . basename($file);
$this->currencies = json_decode(file_get_contents($file), true);
// write to cache
if ($cache_dir && file_exists($cache_dir)) {
file_put_contents($cache_dir . '/' . basename($file), file_get_contents($file));
}
}
/**
* Return an Array with currencySettings
* @param $iso_code
* @return array
*/
public function getCurrencyByIsoCode($iso_code) {
if (array_key_exists(strtolower($iso_code), $this->currencies))
return $this->currencies[strtolower($iso_code)];
throw new UnknownCurrencyException(sprintf('currency with iso-code %s not found', $iso_code));
}
}
\ No newline at end of file
......@@ -28,15 +28,17 @@ class Money
/**
* Create a Money instance
* @param integer $amount Amount, expressed in the smallest units of $currency (eg cents)
* @param \Money\Currency $currency
* @param \Money\Currency|string $currency as Obj or isoString
* @throws \Money\InvalidArgumentException
*/
public function __construct($amount, Currency $currency)
public function __construct($amount, Currency $currency=null)
{
if (!is_int($amount)) {
throw new InvalidArgumentException("The first parameter of Money must be an integer. It's the amount, expressed in the smallest units of currency (eg cents)");
}
$this->amount = $amount;
if (!$currency instanceof Currency)
$currency = new Currency($currency ? : Currency::getDefaultCurrency());
$this->currency = $currency;
}
......@@ -125,12 +127,24 @@ class Money
return $this->amount;
}
/**
* @return int
*/
public function getAmount()
/**
* @param bool $asUnit - default as (int)cent for internals, with true for exernals(string) (formfields etc) as $, EUR etc.
* if subunit == 0 -> strip decimal places
* @param array $params
* force_decimal -> add ,00 if cents are empty ("0"-quantitiy from decimalPlaces)
* @return int|string
*/
public function getAmount($asUnit=false, $params=array())
{
return $this->amount;
if (!$asUnit)
return $this->amount;
// without decimal if == 0 if params dont force
$subunit = (string)floor($this->amount) % $this->currency->getSubunitToUnit();
if (($subunit == 0 && (!isset($params['force_decimal']) || !$params['force_decimal'])) || $this->currency->getDecimalPlaces() == 0)
return (string)floor($this->amount) / $this->currency->getSubunitToUnit();
return (string)number_format($this->amount / $this->currency->getSubunitToUnit(), $this->currency->getDecimalPlaces(), $this->currency->getDecimalMark(), '');
}
/**
......@@ -273,4 +287,113 @@ class Money
return (int) $units;
}
/**
* @see Money::stringToUnits()
*/
public static function parseMoneyString($string) {
return self::stringToUnits($string);
}
/**
* returns the formatted Money Amount
* @param array $params
* display_free => shows 'free' if amount == 0
* html => display html formatted
* symbol => user_defined symbol
* no_cents => show no cents
* no_cents_if_zero => show no cents if ==00
* thousands_separator => overwrite the currency thousands_separator
* decimal_mark => overwrite the currency decimal_mark
* with_currency => append currency ISOCODE
* @return mixed|string
*/
public function format($params = array()) {
// show 'free'
if ($this->amount === 0) {
if (is_string($params['display_free']))
return $params['display_free'];
elseif (isset($params['display_free']) && $params['display_free'])
return "free";
}
// html symbol
$symbolValue = $this->currency->getSymbol(isset($params['html']) && $params['html']);
// userdefined symbol
if (isset($params['symbol']) && $params['symbol'] !== true) {
if (!$params['symbol'])
$symbolValue = '';
else
$symbolValue = $params['symbol'];
}
// show no_cents
if (isset($params['no_cents']) && $params['no_cents'] === true) {
$formatted = (string)floor($this->getAmount(true));
// no_cents if ==00 -> default behjaviour from getAmount(true)
} elseif (isset($params['no_cents_if_zero']) && $params['no_cents_if_zero'] === true && $this->amount % $this->currency->getSubunitToUnit() == 0) {
$formatted = (string)$this->getAmount(true);
} else {
$formatted = $this->getAmount(true, array('force_decimal' => true));
}
// warp span arrount amount if html
if (isset($params['html']) && $params['html']) {
$formatted = '<span class="amount">' . $formatted . '</span>';
}
if (isset($params['symbol_position']))
$symbolPosition = $params['symbol_position'];
else
$symbolPosition = $this->currency->getSymbolPosition(true);
// wrap span arround symbol if html
if (isset($params['html']) && $params['html']) {
$symbolValue = '<span class="symbol">' . $symbolValue . '</span>';
}
// combine symbol and formatted amount
if (isset($symbolValue) && !empty($symbolValue)) {
$formatted = $symbolPosition === 'before'
? "$symbolValue$formatted"
: "$formatted$symbolValue";
}
if (isset($params['decimal_mark']) && $params['decimal_mark'] && $params['decimal_mark'] !== $this->currency->getDecimalMark()) {
$tmp = 1; /* Needs to be pass by ref */
$formatted = str_replace($this->currency->getDecimalMark(), $params['decimal_mark'], $formatted, $tmp);
}
$thousandsSeparatorValue = $this->getCurrency()->getThousandsSeparator();
if (isset($params['thousands_separator'])) {
if ($params['thousands_separator'] === false || $params['thousands_separator'] === null)
$thousandsSeparatorValue = '';
elseif ($params['thousands_separator'])
$thousandsSeparatorValue = $params['thousands_separator'];
}
$formatted = preg_replace('/(\d)(?=(?:\d{3})+(?:[^\d]|$))/', '\1' . $thousandsSeparatorValue, $formatted);
if (isset($params['with_currency']) && $params['with_currency']) {
if (isset($params['html']) && $params['html'])
$formatted .= '<span class="currency">'. $this->currency->__toString(). '</span>';
else
$formatted .= $this->currency->__toString();
}
return $formatted;
}
/**
* build string represantiation of the amount without formatting and currency sign
* @return string
*/
public function __toString() {
return $this->getAmount(true);
}
}
<?php
/**
* This file is part of the Money library
*
* Copyright (c) 2013 Thorsten Buss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Money;
class UnknownCurrencyExtendedValueException extends \Exception implements Exception { }
......@@ -10,13 +10,21 @@
namespace Money\Tests;
use Money\CurrencyLookupRubyMoney;
use PHPUnit_Framework_TestCase;
use Money\Money;
use Money\Currency;
class MoneyTest extends PHPUnit_Framework_TestCase
{
public function testFactoryMethods()
protected function setUp() {
parent::setUp();
// load RubyMoney CurrencyList and register lookup
Currency::setCurrencyLookup(new CurrencyLookupRubyMoney());
}
public function testFactoryMethods()
{
$this->assertEquals(
Money::EUR(25),
......@@ -236,4 +244,47 @@ class MoneyTest extends PHPUnit_Framework_TestCase
{
$this->assertEquals($units, Money::stringToUnits($string));
}
public function testGetAmount() {
/** @var Money $m */
$m = Money::USD(123456); // $1234.56
$this->assertEquals('1234.56', $m->getAmount(true));
$m = Money::USD(-123456); // $1234.56
$this->assertEquals('-1234.56', $m->getAmount(true));
$m = Money::USD(10023456); // $1234.56
$this->assertEquals('100234.56', $m->getAmount(true));
$m = Money::USD(-10023456); // $1234.56
$this->assertEquals('-100234.56', $m->getAmount(true));
$m = Money::USD(123400); // $1234.00
$this->assertEquals('1234', $m->getAmount(true));
$m = Money::USD(-123400); // $1234.00
$this->assertEquals('-1234', $m->getAmount(true));
}
public function testFormat() {
/** @var Money $m1 */
$m1 = Money::USD(123456); // $1234.56
/** @var Money $m2 */
$m2 = Money::USD(123400); // $1234.00
$this->assertEquals('$1,234.56', $m1->format());
$this->assertEquals('$1,234~56', $m1->format(array('decimal_mark' => '~')));
$this->assertEquals('$1_234.56', $m1->format(array('thousands_separator' => '_')));
$this->assertEquals('$1,234.56USD', $m1->format(array('with_currency' => true)));
$this->assertEquals('<span class="symbol">$</span><span class="amount">1,234.56</span><span class="currency">USD</span>', $m1->format(array('with_currency' => true, 'html' => true)));