Viewing file: Money.php (15.6 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
namespace Money;
use Money\Calculator\BcMathCalculator; use Money\Calculator\GmpCalculator; use Money\Calculator\PhpCalculator;
/** * Money Value Object. * * @author Mathias Verraes * * @psalm-immutable */ final class Money implements \JsonSerializable { use MoneyFactory;
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; }
/** * 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) > 0; }
/** * @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) < 0; }
/** * @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[] $addends * * @return Money */ public function add(Money ...$addends) { $amount = $this->amount; $calculator = $this->getCalculator();
foreach ($addends as $addend) { $this->assertSameCurrency($addend);
$amount = $calculator->add($amount, $addend->amount); }
return new self($amount, $this->currency); }
/** * Returns a new Money object that represents * the difference of this and an other Money object. * * @param Money[] $subtrahends * * @return Money * * @psalm-pure */ public function subtract(Money ...$subtrahends) { $amount = $this->amount; $calculator = $this->getCalculator();
foreach ($subtrahends as $subtrahend) { $this->assertSameCurrency($subtrahend);
$amount = $calculator->subtract($amount, $subtrahend->amount); }
return new self($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);
$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);
$divisor = (string) Number::fromNumber($divisor);
if ($this->getCalculator()->compare($divisor, '0') === 0) { throw new \InvalidArgumentException('Division by zero'); }
$quotient = $this->round($this->getCalculator()->divide($this->amount, $divisor), $roundingMode);
return $this->newInstance($quotient); }
/** * Returns a new Money object that represents * the remainder after dividing the value by * the given factor. * * @param Money $divisor * * @return Money */ public function mod(Money $divisor) { $this->assertSameCurrency($divisor);
return new self($this->getCalculator()->mod($this->amount, $divisor->amount), $this->currency); }
/** * 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 $key => $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[$key] = $this->newInstance($share); $remainder = $this->getCalculator()->subtract($remainder, $share); }
if ($this->getCalculator()->compare($remainder, '0') === 0) { return $results; }
$fractions = array_map(function ($ratio) use ($total) { $share = ($ratio / $total) * $this->amount;
return $share - floor($share); }, $ratios);
while ($this->getCalculator()->compare($remainder, '0') > 0) { $index = !empty($fractions) ? array_keys($fractions, max($fractions))[0] : 0; $results[$index]->amount = $this->getCalculator()->add($results[$index]->amount, '1'); $remainder = $this->getCalculator()->subtract($remainder, '1'); unset($fractions[$index]); }
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 Money $money * * @return string */ public function ratioOf(Money $money) { if ($money->isZero()) { throw new \InvalidArgumentException('Cannot calculate a ratio of zero'); }
return $this->getCalculator()->divide($this->amount, $money->amount); }
/** * @param string $amount * @param int $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) > 0; }
/** * Checks if the value represented by this object is negative. * * @return bool */ public function isNegative() { return $this->getCalculator()->compare($this->amount, 0) < 0; }
/** * {@inheritdoc} * * @return array */ public function jsonSerialize() { return [ 'amount' => $this->amount, 'currency' => $this->currency->jsonSerialize(), ]; }
/** * @param Money $first * @param Money ...$collection * * @return Money * * @psalm-pure */ public static function min(self $first, self ...$collection) { $min = $first;
foreach ($collection as $money) { if ($money->lessThan($min)) { $min = $money; } }
return $min; }
/** * @param Money $first * @param Money ...$collection * * @return Money * * @psalm-pure */ public static function max(self $first, self ...$collection) { $max = $first;
foreach ($collection as $money) { if ($money->greaterThan($max)) { $max = $money; } }
return $max; }
/** * @param Money $first * @param Money ...$collection * * @return Money * * @psalm-pure */ public static function sum(self $first, self ...$collection) { return $first->add(...$collection); }
/** * @param Money $first * @param Money ...$collection * * @return Money * * @psalm-pure */ public static function avg(self $first, self ...$collection) { return $first->add(...$collection)->divide(func_num_args()); }
/** * @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; } }
|