vendor/moneyphp/money/src/Money.php line 521

Open in your IDE?
  1. <?php
  2. namespace Money;
  3. use Money\Calculator\BcMathCalculator;
  4. use Money\Calculator\GmpCalculator;
  5. use Money\Calculator\PhpCalculator;
  6. /**
  7.  * Money Value Object.
  8.  *
  9.  * @author Mathias Verraes
  10.  *
  11.  * @psalm-immutable
  12.  */
  13. final class Money implements \JsonSerializable
  14. {
  15.     use MoneyFactory;
  16.     const ROUND_HALF_UP PHP_ROUND_HALF_UP;
  17.     const ROUND_HALF_DOWN PHP_ROUND_HALF_DOWN;
  18.     const ROUND_HALF_EVEN PHP_ROUND_HALF_EVEN;
  19.     const ROUND_HALF_ODD PHP_ROUND_HALF_ODD;
  20.     const ROUND_UP 5;
  21.     const ROUND_DOWN 6;
  22.     const ROUND_HALF_POSITIVE_INFINITY 7;
  23.     const ROUND_HALF_NEGATIVE_INFINITY 8;
  24.     /**
  25.      * Internal value.
  26.      *
  27.      * @var string
  28.      */
  29.     private $amount;
  30.     /**
  31.      * @var Currency
  32.      */
  33.     private $currency;
  34.     /**
  35.      * @var Calculator
  36.      */
  37.     private static $calculator;
  38.     /**
  39.      * @var array
  40.      */
  41.     private static $calculators = [
  42.         BcMathCalculator::class,
  43.         GmpCalculator::class,
  44.         PhpCalculator::class,
  45.     ];
  46.     /**
  47.      * @param int|string $amount   Amount, expressed in the smallest units of $currency (eg cents)
  48.      * @param Currency   $currency
  49.      *
  50.      * @throws \InvalidArgumentException If amount is not integer
  51.      */
  52.     public function __construct($amountCurrency $currency)
  53.     {
  54.         if (filter_var($amountFILTER_VALIDATE_INT) === false) {
  55.             $numberFromString Number::fromString($amount);
  56.             if (!$numberFromString->isInteger()) {
  57.                 throw new \InvalidArgumentException('Amount must be an integer(ish) value');
  58.             }
  59.             $amount $numberFromString->getIntegerPart();
  60.         }
  61.         $this->amount = (string) $amount;
  62.         $this->currency $currency;
  63.     }
  64.     /**
  65.      * Returns a new Money instance based on the current one using the Currency.
  66.      *
  67.      * @param int|string $amount
  68.      *
  69.      * @return Money
  70.      *
  71.      * @throws \InvalidArgumentException If amount is not integer
  72.      */
  73.     private function newInstance($amount)
  74.     {
  75.         return new self($amount$this->currency);
  76.     }
  77.     /**
  78.      * Checks whether a Money has the same Currency as this.
  79.      *
  80.      * @param Money $other
  81.      *
  82.      * @return bool
  83.      */
  84.     public function isSameCurrency(Money $other)
  85.     {
  86.         return $this->currency->equals($other->currency);
  87.     }
  88.     /**
  89.      * Asserts that a Money has the same currency as this.
  90.      *
  91.      * @param Money $other
  92.      *
  93.      * @throws \InvalidArgumentException If $other has a different currency
  94.      */
  95.     private function assertSameCurrency(Money $other)
  96.     {
  97.         if (!$this->isSameCurrency($other)) {
  98.             throw new \InvalidArgumentException('Currencies must be identical');
  99.         }
  100.     }
  101.     /**
  102.      * Checks whether the value represented by this object equals to the other.
  103.      *
  104.      * @param Money $other
  105.      *
  106.      * @return bool
  107.      */
  108.     public function equals(Money $other)
  109.     {
  110.         return $this->isSameCurrency($other) && $this->amount === $other->amount;
  111.     }
  112.     /**
  113.      * Returns an integer less than, equal to, or greater than zero
  114.      * if the value of this object is considered to be respectively
  115.      * less than, equal to, or greater than the other.
  116.      *
  117.      * @param Money $other
  118.      *
  119.      * @return int
  120.      */
  121.     public function compare(Money $other)
  122.     {
  123.         $this->assertSameCurrency($other);
  124.         return $this->getCalculator()->compare($this->amount$other->amount);
  125.     }
  126.     /**
  127.      * Checks whether the value represented by this object is greater than the other.
  128.      *
  129.      * @param Money $other
  130.      *
  131.      * @return bool
  132.      */
  133.     public function greaterThan(Money $other)
  134.     {
  135.         return $this->compare($other) > 0;
  136.     }
  137.     /**
  138.      * @param \Money\Money $other
  139.      *
  140.      * @return bool
  141.      */
  142.     public function greaterThanOrEqual(Money $other)
  143.     {
  144.         return $this->compare($other) >= 0;
  145.     }
  146.     /**
  147.      * Checks whether the value represented by this object is less than the other.
  148.      *
  149.      * @param Money $other
  150.      *
  151.      * @return bool
  152.      */
  153.     public function lessThan(Money $other)
  154.     {
  155.         return $this->compare($other) < 0;
  156.     }
  157.     /**
  158.      * @param \Money\Money $other
  159.      *
  160.      * @return bool
  161.      */
  162.     public function lessThanOrEqual(Money $other)
  163.     {
  164.         return $this->compare($other) <= 0;
  165.     }
  166.     /**
  167.      * Returns the value represented by this object.
  168.      *
  169.      * @return string
  170.      */
  171.     public function getAmount()
  172.     {
  173.         return $this->amount;
  174.     }
  175.     /**
  176.      * Returns the currency of this object.
  177.      *
  178.      * @return Currency
  179.      */
  180.     public function getCurrency()
  181.     {
  182.         return $this->currency;
  183.     }
  184.     /**
  185.      * Returns a new Money object that represents
  186.      * the sum of this and an other Money object.
  187.      *
  188.      * @param Money[] $addends
  189.      *
  190.      * @return Money
  191.      */
  192.     public function add(Money ...$addends)
  193.     {
  194.         $amount $this->amount;
  195.         $calculator $this->getCalculator();
  196.         foreach ($addends as $addend) {
  197.             $this->assertSameCurrency($addend);
  198.             $amount $calculator->add($amount$addend->amount);
  199.         }
  200.         return new self($amount$this->currency);
  201.     }
  202.     /**
  203.      * Returns a new Money object that represents
  204.      * the difference of this and an other Money object.
  205.      *
  206.      * @param Money[] $subtrahends
  207.      *
  208.      * @return Money
  209.      *
  210.      * @psalm-pure
  211.      */
  212.     public function subtract(Money ...$subtrahends)
  213.     {
  214.         $amount $this->amount;
  215.         $calculator $this->getCalculator();
  216.         foreach ($subtrahends as $subtrahend) {
  217.             $this->assertSameCurrency($subtrahend);
  218.             $amount $calculator->subtract($amount$subtrahend->amount);
  219.         }
  220.         return new self($amount$this->currency);
  221.     }
  222.     /**
  223.      * Asserts that the operand is integer or float.
  224.      *
  225.      * @param float|int|string $operand
  226.      *
  227.      * @throws \InvalidArgumentException If $operand is neither integer nor float
  228.      */
  229.     private function assertOperand($operand)
  230.     {
  231.         if (!is_numeric($operand)) {
  232.             throw new \InvalidArgumentException(sprintf(
  233.                 'Operand should be a numeric value, "%s" given.',
  234.                 is_object($operand) ? get_class($operand) : gettype($operand)
  235.             ));
  236.         }
  237.     }
  238.     /**
  239.      * Asserts that rounding mode is a valid integer value.
  240.      *
  241.      * @param int $roundingMode
  242.      *
  243.      * @throws \InvalidArgumentException If $roundingMode is not valid
  244.      */
  245.     private function assertRoundingMode($roundingMode)
  246.     {
  247.         if (!in_array(
  248.             $roundingMode, [
  249.                 self::ROUND_HALF_DOWNself::ROUND_HALF_EVENself::ROUND_HALF_ODD,
  250.                 self::ROUND_HALF_UPself::ROUND_UPself::ROUND_DOWN,
  251.                 self::ROUND_HALF_POSITIVE_INFINITYself::ROUND_HALF_NEGATIVE_INFINITY,
  252.             ], true
  253.         )) {
  254.             throw new \InvalidArgumentException(
  255.                 'Rounding mode should be Money::ROUND_HALF_DOWN | '.
  256.                 'Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | '.
  257.                 'Money::ROUND_HALF_UP | Money::ROUND_UP | Money::ROUND_DOWN'.
  258.                 'Money::ROUND_HALF_POSITIVE_INFINITY | Money::ROUND_HALF_NEGATIVE_INFINITY'
  259.             );
  260.         }
  261.     }
  262.     /**
  263.      * Returns a new Money object that represents
  264.      * the multiplied value by the given factor.
  265.      *
  266.      * @param float|int|string $multiplier
  267.      * @param int              $roundingMode
  268.      *
  269.      * @return Money
  270.      */
  271.     public function multiply($multiplier$roundingMode self::ROUND_HALF_UP)
  272.     {
  273.         $this->assertOperand($multiplier);
  274.         $this->assertRoundingMode($roundingMode);
  275.         $product $this->round($this->getCalculator()->multiply($this->amount$multiplier), $roundingMode);
  276.         return $this->newInstance($product);
  277.     }
  278.     /**
  279.      * Returns a new Money object that represents
  280.      * the divided value by the given factor.
  281.      *
  282.      * @param float|int|string $divisor
  283.      * @param int              $roundingMode
  284.      *
  285.      * @return Money
  286.      */
  287.     public function divide($divisor$roundingMode self::ROUND_HALF_UP)
  288.     {
  289.         $this->assertOperand($divisor);
  290.         $this->assertRoundingMode($roundingMode);
  291.         $divisor = (string) Number::fromNumber($divisor);
  292.         if ($this->getCalculator()->compare($divisor'0') === 0) {
  293.             throw new \InvalidArgumentException('Division by zero');
  294.         }
  295.         $quotient $this->round($this->getCalculator()->divide($this->amount$divisor), $roundingMode);
  296.         return $this->newInstance($quotient);
  297.     }
  298.     /**
  299.      * Returns a new Money object that represents
  300.      * the remainder after dividing the value by
  301.      * the given factor.
  302.      *
  303.      * @param Money $divisor
  304.      *
  305.      * @return Money
  306.      */
  307.     public function mod(Money $divisor)
  308.     {
  309.         $this->assertSameCurrency($divisor);
  310.         return new self($this->getCalculator()->mod($this->amount$divisor->amount), $this->currency);
  311.     }
  312.     /**
  313.      * Allocate the money according to a list of ratios.
  314.      *
  315.      * @param array $ratios
  316.      *
  317.      * @return Money[]
  318.      */
  319.     public function allocate(array $ratios)
  320.     {
  321.         if (count($ratios) === 0) {
  322.             throw new \InvalidArgumentException('Cannot allocate to none, ratios cannot be an empty array');
  323.         }
  324.         $remainder $this->amount;
  325.         $results = [];
  326.         $total array_sum($ratios);
  327.         if ($total <= 0) {
  328.             throw new \InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero');
  329.         }
  330.         foreach ($ratios as $key => $ratio) {
  331.             if ($ratio 0) {
  332.                 throw new \InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive');
  333.             }
  334.             $share $this->getCalculator()->share($this->amount$ratio$total);
  335.             $results[$key] = $this->newInstance($share);
  336.             $remainder $this->getCalculator()->subtract($remainder$share);
  337.         }
  338.         if ($this->getCalculator()->compare($remainder'0') === 0) {
  339.             return $results;
  340.         }
  341.         $fractions array_map(function ($ratio) use ($total) {
  342.             $share = ($ratio $total) * $this->amount;
  343.             return $share floor($share);
  344.         }, $ratios);
  345.         while ($this->getCalculator()->compare($remainder'0') > 0) {
  346.             $index = !empty($fractions) ? array_keys($fractionsmax($fractions))[0] : 0;
  347.             $results[$index]->amount $this->getCalculator()->add($results[$index]->amount'1');
  348.             $remainder $this->getCalculator()->subtract($remainder'1');
  349.             unset($fractions[$index]);
  350.         }
  351.         return $results;
  352.     }
  353.     /**
  354.      * Allocate the money among N targets.
  355.      *
  356.      * @param int $n
  357.      *
  358.      * @return Money[]
  359.      *
  360.      * @throws \InvalidArgumentException If number of targets is not an integer
  361.      */
  362.     public function allocateTo($n)
  363.     {
  364.         if (!is_int($n)) {
  365.             throw new \InvalidArgumentException('Number of targets must be an integer');
  366.         }
  367.         if ($n <= 0) {
  368.             throw new \InvalidArgumentException('Cannot allocate to none, target must be greater than zero');
  369.         }
  370.         return $this->allocate(array_fill(0$n1));
  371.     }
  372.     /**
  373.      * @param Money $money
  374.      *
  375.      * @return string
  376.      */
  377.     public function ratioOf(Money $money)
  378.     {
  379.         if ($money->isZero()) {
  380.             throw new \InvalidArgumentException('Cannot calculate a ratio of zero');
  381.         }
  382.         return $this->getCalculator()->divide($this->amount$money->amount);
  383.     }
  384.     /**
  385.      * @param string $amount
  386.      * @param int    $rounding_mode
  387.      *
  388.      * @return string
  389.      */
  390.     private function round($amount$rounding_mode)
  391.     {
  392.         $this->assertRoundingMode($rounding_mode);
  393.         if ($rounding_mode === self::ROUND_UP) {
  394.             return $this->getCalculator()->ceil($amount);
  395.         }
  396.         if ($rounding_mode === self::ROUND_DOWN) {
  397.             return $this->getCalculator()->floor($amount);
  398.         }
  399.         return $this->getCalculator()->round($amount$rounding_mode);
  400.     }
  401.     /**
  402.      * @return Money
  403.      */
  404.     public function absolute()
  405.     {
  406.         return $this->newInstance($this->getCalculator()->absolute($this->amount));
  407.     }
  408.     /**
  409.      * @return Money
  410.      */
  411.     public function negative()
  412.     {
  413.         return $this->newInstance(0)->subtract($this);
  414.     }
  415.     /**
  416.      * Checks if the value represented by this object is zero.
  417.      *
  418.      * @return bool
  419.      */
  420.     public function isZero()
  421.     {
  422.         return $this->getCalculator()->compare($this->amount0) === 0;
  423.     }
  424.     /**
  425.      * Checks if the value represented by this object is positive.
  426.      *
  427.      * @return bool
  428.      */
  429.     public function isPositive()
  430.     {
  431.         return $this->getCalculator()->compare($this->amount0) > 0;
  432.     }
  433.     /**
  434.      * Checks if the value represented by this object is negative.
  435.      *
  436.      * @return bool
  437.      */
  438.     public function isNegative()
  439.     {
  440.         return $this->getCalculator()->compare($this->amount0) < 0;
  441.     }
  442.     /**
  443.      * {@inheritdoc}
  444.      *
  445.      * @return array
  446.      */
  447.     public function jsonSerialize()
  448.     {
  449.         return [
  450.             'amount' => $this->amount,
  451.             'currency' => $this->currency->jsonSerialize(),
  452.         ];
  453.     }
  454.     /**
  455.      * @param Money $first
  456.      * @param Money ...$collection
  457.      *
  458.      * @return Money
  459.      *
  460.      * @psalm-pure
  461.      */
  462.     public static function min(self $firstself ...$collection)
  463.     {
  464.         $min $first;
  465.         foreach ($collection as $money) {
  466.             if ($money->lessThan($min)) {
  467.                 $min $money;
  468.             }
  469.         }
  470.         return $min;
  471.     }
  472.     /**
  473.      * @param Money $first
  474.      * @param Money ...$collection
  475.      *
  476.      * @return Money
  477.      *
  478.      * @psalm-pure
  479.      */
  480.     public static function max(self $firstself ...$collection)
  481.     {
  482.         $max $first;
  483.         foreach ($collection as $money) {
  484.             if ($money->greaterThan($max)) {
  485.                 $max $money;
  486.             }
  487.         }
  488.         return $max;
  489.     }
  490.     /**
  491.      * @param Money $first
  492.      * @param Money ...$collection
  493.      *
  494.      * @return Money
  495.      *
  496.      * @psalm-pure
  497.      */
  498.     public static function sum(self $firstself ...$collection)
  499.     {
  500.         return $first->add(...$collection);
  501.     }
  502.     /**
  503.      * @param Money $first
  504.      * @param Money ...$collection
  505.      *
  506.      * @return Money
  507.      *
  508.      * @psalm-pure
  509.      */
  510.     public static function avg(self $firstself ...$collection)
  511.     {
  512.         return $first->add(...$collection)->divide(func_num_args());
  513.     }
  514.     /**
  515.      * @param string $calculator
  516.      */
  517.     public static function registerCalculator($calculator)
  518.     {
  519.         if (is_a($calculatorCalculator::class, true) === false) {
  520.             throw new \InvalidArgumentException('Calculator must implement '.Calculator::class);
  521.         }
  522.         array_unshift(self::$calculators$calculator);
  523.     }
  524.     /**
  525.      * @return Calculator
  526.      *
  527.      * @throws \RuntimeException If cannot find calculator for money calculations
  528.      */
  529.     private static function initializeCalculator()
  530.     {
  531.         $calculators self::$calculators;
  532.         foreach ($calculators as $calculator) {
  533.             /** @var Calculator $calculator */
  534.             if ($calculator::supported()) {
  535.                 return new $calculator();
  536.             }
  537.         }
  538.         throw new \RuntimeException('Cannot find calculator for money calculations');
  539.     }
  540.     /**
  541.      * @return Calculator
  542.      */
  543.     private function getCalculator()
  544.     {
  545.         if (null === self::$calculator) {
  546.             self::$calculator self::initializeCalculator();
  547.         }
  548.         return self::$calculator;
  549.     }
  550. }