Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
70.97% |
22 / 31 |
CRAP | |
77.12% |
91 / 118 |
| Money | |
0.00% |
0 / 1 |
|
70.97% |
22 / 31 |
88.93 | |
77.12% |
91 / 118 |
| __construct | |
0.00% |
0 / 1 |
3.01 | |
88.89% |
8 / 9 |
|||
| __callStatic | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| newInstance | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| isSameCurrency | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| assertSameCurrency | |
0.00% |
0 / 1 |
2.15 | |
66.67% |
2 / 3 |
|||
| equals | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
| compare | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| greaterThan | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| greaterThanOrEqual | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| lessThan | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| lessThanOrEqual | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| getAmount | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| getCurrency | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| add | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| subtract | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| assertOperand | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
| assertRoundingMode | |
0.00% |
0 / 1 |
2.15 | |
66.67% |
8 / 12 |
|||
| multiply | |
100.00% |
1 / 1 |
2 | |
100.00% |
7 / 7 |
|||
| divide | |
0.00% |
0 / 1 |
9.32 | |
11.11% |
1 / 9 |
|||
| allocate | |
0.00% |
0 / 1 |
6.14 | |
84.21% |
16 / 19 |
|||
| allocateTo | |
0.00% |
0 / 1 |
3.58 | |
60.00% |
3 / 5 |
|||
| round | |
0.00% |
0 / 1 |
3.33 | |
66.67% |
4 / 6 |
|||
| absolute | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| negative | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| isZero | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| isPositive | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| isNegative | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| jsonSerialize | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
| registerCalculator | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
| initializeCalculator | |
0.00% |
0 / 1 |
3.33 | |
66.67% |
4 / 6 |
|||
| getCalculator | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| <?php | |
| namespace Money; | |
| use Money\Calculator\BcMathCalculator; | |
| use Money\Calculator\GmpCalculator; | |
| use Money\Calculator\PhpCalculator; | |
| /** | |
| * Money Value Object. | |
| * | |
| * @author Mathias Verraes | |
| */ | |
| final class Money implements \JsonSerializable | |
| { | |
| const ROUND_HALF_UP = PHP_ROUND_HALF_UP; | |
| const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN; | |
| const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN; | |
| const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD; | |
| const ROUND_UP = 5; | |
| const ROUND_DOWN = 6; | |
| const ROUND_HALF_POSITIVE_INFINITY = 7; | |
| const ROUND_HALF_NEGATIVE_INFINITY = 8; | |
| /** | |
| * Internal value. | |
| * | |
| * @var string | |
| */ | |
| private $amount; | |
| /** | |
| * @var Currency | |
| */ | |
| private $currency; | |
| /** | |
| * @var Calculator | |
| */ | |
| private static $calculator; | |
| /** | |
| * @var array | |
| */ | |
| private static $calculators = [ | |
| BcMathCalculator::class, | |
| GmpCalculator::class, | |
| PhpCalculator::class, | |
| ]; | |
| /** | |
| * @param int|string $amount Amount, expressed in the smallest units of $currency (eg cents) | |
| * @param Currency $currency | |
| * | |
| * @throws \InvalidArgumentException If amount is not integer | |
| */ | |
| public function __construct($amount, Currency $currency) | |
| { | |
| if (filter_var($amount, FILTER_VALIDATE_INT) === false) { | |
| $numberFromString = Number::fromString($amount); | |
| if (!$numberFromString->isInteger()) { | |
| throw new \InvalidArgumentException('Amount must be an integer(ish) value'); | |
| } | |
| $amount = $numberFromString->getIntegerPart(); | |
| } | |
| $this->amount = (string) $amount; | |
| $this->currency = $currency; | |
| } | |
| /** | |
| * Convenience factory method for a Money object. | |
| * | |
| * <code> | |
| * $fiveDollar = Money::USD(500); | |
| * </code> | |
| * | |
| * @param string $method | |
| * @param array $arguments | |
| * | |
| * @return Money | |
| * | |
| * @throws \InvalidArgumentException If amount is not integer | |
| */ | |
| public static function __callStatic($method, $arguments) | |
| { | |
| return new self($arguments[0], new Currency($method)); | |
| } | |
| /** | |
| * Returns a new Money instance based on the current one using the Currency. | |
| * | |
| * @param int|string $amount | |
| * | |
| * @return Money | |
| * | |
| * @throws \InvalidArgumentException If amount is not integer | |
| */ | |
| private function newInstance($amount) | |
| { | |
| return new self($amount, $this->currency); | |
| } | |
| /** | |
| * Checks whether a Money has the same Currency as this. | |
| * | |
| * @param Money $other | |
| * | |
| * @return bool | |
| */ | |
| public function isSameCurrency(Money $other) | |
| { | |
| return $this->currency->equals($other->currency); | |
| } | |
| /** | |
| * Asserts that a Money has the same currency as this. | |
| * | |
| * @param Money $other | |
| * | |
| * @throws \InvalidArgumentException If $other has a different currency | |
| */ | |
| private function assertSameCurrency(Money $other) | |
| { | |
| if (!$this->isSameCurrency($other)) { | |
| throw new \InvalidArgumentException('Currencies must be identical'); | |
| } | |
| } | |
| /** | |
| * Checks whether the value represented by this object equals to the other. | |
| * | |
| * @param Money $other | |
| * | |
| * @return bool | |
| */ | |
| public function equals(Money $other) | |
| { | |
| return $this->isSameCurrency($other) && $this->amount === $other->amount; | |
| } | |
| /** | |
| * Returns an integer less than, equal to, or greater than zero | |
| * if the value of this object is considered to be respectively | |
| * less than, equal to, or greater than the other. | |
| * | |
| * @param Money $other | |
| * | |
| * @return int | |
| */ | |
| public function compare(Money $other) | |
| { | |
| $this->assertSameCurrency($other); | |
| return $this->getCalculator()->compare($this->amount, $other->amount); | |
| } | |
| /** | |
| * Checks whether the value represented by this object is greater than the other. | |
| * | |
| * @param Money $other | |
| * | |
| * @return bool | |
| */ | |
| public function greaterThan(Money $other) | |
| { | |
| return $this->compare($other) === 1; | |
| } | |
| /** | |
| * @param \Money\Money $other | |
| * | |
| * @return bool | |
| */ | |
| public function greaterThanOrEqual(Money $other) | |
| { | |
| return $this->compare($other) >= 0; | |
| } | |
| /** | |
| * Checks whether the value represented by this object is less than the other. | |
| * | |
| * @param Money $other | |
| * | |
| * @return bool | |
| */ | |
| public function lessThan(Money $other) | |
| { | |
| return $this->compare($other) === -1; | |
| } | |
| /** | |
| * @param \Money\Money $other | |
| * | |
| * @return bool | |
| */ | |
| public function lessThanOrEqual(Money $other) | |
| { | |
| return $this->compare($other) <= 0; | |
| } | |
| /** | |
| * Returns the value represented by this object. | |
| * | |
| * @return string | |
| */ | |
| public function getAmount() | |
| { | |
| return $this->amount; | |
| } | |
| /** | |
| * Returns the currency of this object. | |
| * | |
| * @return Currency | |
| */ | |
| public function getCurrency() | |
| { | |
| return $this->currency; | |
| } | |
| /** | |
| * Returns a new Money object that represents | |
| * the sum of this and an other Money object. | |
| * | |
| * @param Money $addend | |
| * | |
| * @return Money | |
| */ | |
| public function add(Money $addend) | |
| { | |
| $this->assertSameCurrency($addend); | |
| return new self($this->getCalculator()->add($this->amount, $addend->amount), $this->currency); | |
| } | |
| /** | |
| * Returns a new Money object that represents | |
| * the difference of this and an other Money object. | |
| * | |
| * @param Money $subtrahend | |
| * | |
| * @return Money | |
| */ | |
| public function subtract(Money $subtrahend) | |
| { | |
| $this->assertSameCurrency($subtrahend); | |
| return new self($this->getCalculator()->subtract($this->amount, $subtrahend->amount), $this->currency); | |
| } | |
| /** | |
| * Asserts that the operand is integer or float. | |
| * | |
| * @param float|int|string $operand | |
| * | |
| * @throws \InvalidArgumentException If $operand is neither integer nor float | |
| */ | |
| private function assertOperand($operand) | |
| { | |
| if (!is_numeric($operand)) { | |
| throw new \InvalidArgumentException(sprintf( | |
| 'Operand should be a numeric value, "%s" given.', | |
| is_object($operand) ? get_class($operand) : gettype($operand) | |
| )); | |
| } | |
| } | |
| /** | |
| * Asserts that rounding mode is a valid integer value. | |
| * | |
| * @param int $roundingMode | |
| * | |
| * @throws \InvalidArgumentException If $roundingMode is not valid | |
| */ | |
| private function assertRoundingMode($roundingMode) | |
| { | |
| if (!in_array( | |
| $roundingMode, [ | |
| self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD, | |
| self::ROUND_HALF_UP, self::ROUND_UP, self::ROUND_DOWN, | |
| self::ROUND_HALF_POSITIVE_INFINITY, self::ROUND_HALF_NEGATIVE_INFINITY, | |
| ], true | |
| )) { | |
| throw new \InvalidArgumentException( | |
| 'Rounding mode should be Money::ROUND_HALF_DOWN | '. | |
| 'Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | '. | |
| 'Money::ROUND_HALF_UP | Money::ROUND_UP | Money::ROUND_DOWN'. | |
| 'Money::ROUND_HALF_POSITIVE_INFINITY | Money::ROUND_HALF_NEGATIVE_INFINITY' | |
| ); | |
| } | |
| } | |
| /** | |
| * Returns a new Money object that represents | |
| * the multiplied value by the given factor. | |
| * | |
| * @param float|int|string $multiplier | |
| * @param int $roundingMode | |
| * | |
| * @return Money | |
| */ | |
| public function multiply($multiplier, $roundingMode = self::ROUND_HALF_UP) | |
| { | |
| $this->assertOperand($multiplier); | |
| $this->assertRoundingMode($roundingMode); | |
| if (is_float($multiplier)) { | |
| $multiplier = (string) Number::fromFloat($multiplier); | |
| } | |
| $product = $this->round($this->getCalculator()->multiply($this->amount, $multiplier), $roundingMode); | |
| return $this->newInstance($product); | |
| } | |
| /** | |
| * Returns a new Money object that represents | |
| * the divided value by the given factor. | |
| * | |
| * @param float|int|string $divisor | |
| * @param int $roundingMode | |
| * | |
| * @return Money | |
| */ | |
| public function divide($divisor, $roundingMode = self::ROUND_HALF_UP) | |
| { | |
| $this->assertOperand($divisor); | |
| $this->assertRoundingMode($roundingMode); | |
| if (is_float($divisor)) { | |
| $divisor = (string) Number::fromFloat($divisor); | |
| } | |
| if ($this->getCalculator()->compare((string) $divisor, '0') === 0) { | |
| throw new \InvalidArgumentException('Division by zero'); | |
| } | |
| $quotient = $this->round($this->getCalculator()->divide($this->amount, $divisor), $roundingMode); | |
| return $this->newInstance($quotient); | |
| } | |
| /** | |
| * Allocate the money according to a list of ratios. | |
| * | |
| * @param array $ratios | |
| * | |
| * @return Money[] | |
| */ | |
| public function allocate(array $ratios) | |
| { | |
| if (count($ratios) === 0) { | |
| throw new \InvalidArgumentException('Cannot allocate to none, ratios cannot be an empty array'); | |
| } | |
| $remainder = $this->amount; | |
| $results = []; | |
| $total = array_sum($ratios); | |
| if ($total <= 0) { | |
| throw new \InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero'); | |
| } | |
| foreach ($ratios as $ratio) { | |
| if ($ratio < 0) { | |
| throw new \InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive'); | |
| } | |
| $share = $this->getCalculator()->share($this->amount, $ratio, $total); | |
| $results[] = $this->newInstance($share); | |
| $remainder = $this->getCalculator()->subtract($remainder, $share); | |
| } | |
| for ($i = 0; $this->getCalculator()->compare($remainder, 0) === 1; ++$i) { | |
| $results[$i]->amount = (string) $this->getCalculator()->add($results[$i]->amount, 1); | |
| $remainder = $this->getCalculator()->subtract($remainder, 1); | |
| } | |
| return $results; | |
| } | |
| /** | |
| * Allocate the money among N targets. | |
| * | |
| * @param int $n | |
| * | |
| * @return Money[] | |
| * | |
| * @throws \InvalidArgumentException If number of targets is not an integer | |
| */ | |
| public function allocateTo($n) | |
| { | |
| if (!is_int($n)) { | |
| throw new \InvalidArgumentException('Number of targets must be an integer'); | |
| } | |
| if ($n <= 0) { | |
| throw new \InvalidArgumentException('Cannot allocate to none, target must be greater than zero'); | |
| } | |
| return $this->allocate(array_fill(0, $n, 1)); | |
| } | |
| /** | |
| * @param int|float $amount | |
| * @param $rounding_mode | |
| * | |
| * @return string | |
| */ | |
| private function round($amount, $rounding_mode) | |
| { | |
| $this->assertRoundingMode($rounding_mode); | |
| if ($rounding_mode === self::ROUND_UP) { | |
| return $this->getCalculator()->ceil($amount); | |
| } | |
| if ($rounding_mode === self::ROUND_DOWN) { | |
| return $this->getCalculator()->floor($amount); | |
| } | |
| return $this->getCalculator()->round($amount, $rounding_mode); | |
| } | |
| /** | |
| * @return Money | |
| */ | |
| public function absolute() | |
| { | |
| return $this->newInstance($this->getCalculator()->absolute($this->amount)); | |
| } | |
| /** | |
| * @return Money | |
| */ | |
| public function negative() | |
| { | |
| return $this->newInstance(0)->subtract($this); | |
| } | |
| /** | |
| * Checks if the value represented by this object is zero. | |
| * | |
| * @return bool | |
| */ | |
| public function isZero() | |
| { | |
| return $this->getCalculator()->compare($this->amount, 0) === 0; | |
| } | |
| /** | |
| * Checks if the value represented by this object is positive. | |
| * | |
| * @return bool | |
| */ | |
| public function isPositive() | |
| { | |
| return $this->getCalculator()->compare($this->amount, 0) === 1; | |
| } | |
| /** | |
| * Checks if the value represented by this object is negative. | |
| * | |
| * @return bool | |
| */ | |
| public function isNegative() | |
| { | |
| return $this->getCalculator()->compare($this->amount, 0) === -1; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| * | |
| * @return array | |
| */ | |
| public function jsonSerialize() | |
| { | |
| return [ | |
| 'amount' => $this->amount, | |
| 'currency' => $this->currency, | |
| ]; | |
| } | |
| /** | |
| * @param string $calculator | |
| */ | |
| public static function registerCalculator($calculator) | |
| { | |
| if (is_a($calculator, Calculator::class, true) === false) { | |
| throw new \InvalidArgumentException('Calculator must implement '.Calculator::class); | |
| } | |
| array_unshift(self::$calculators, $calculator); | |
| } | |
| /** | |
| * @return Calculator | |
| * | |
| * @throws \RuntimeException If cannot find calculator for money calculations | |
| */ | |
| private static function initializeCalculator() | |
| { | |
| $calculators = self::$calculators; | |
| foreach ($calculators as $calculator) { | |
| /** @var Calculator $calculator */ | |
| if ($calculator::supported()) { | |
| return new $calculator(); | |
| } | |
| } | |
| throw new \RuntimeException('Cannot find calculator for money calculations'); | |
| } | |
| /** | |
| * @return Calculator | |
| */ | |
| private function getCalculator() | |
| { | |
| if (null === self::$calculator) { | |
| self::$calculator = self::initializeCalculator(); | |
| } | |
| return self::$calculator; | |
| } | |
| } |