currency = $currency; $this->amount = $this->parseAmount($amount, $convert); } /** * parseAmount. * * @throws UnexpectedAmountException */ protected function parseAmount(mixed $amount, bool $convert = false): int|float { /** @var int|float|Money $amount */ $amount = $this->parseAmountFromString($this->parseAmountFromCallable($amount)); if (is_int($amount)) { return (int) $this->convertAmount($amount, $convert); } if (is_float($amount)) { return $this->round($this->convertAmount($amount, $convert)); } if ($amount instanceof static) { return $this->convertAmount($amount->getAmount(), $convert); } throw new UnexpectedAmountException('Invalid amount "' . $amount . '"'); } protected function parseAmountFromCallable(mixed $amount): mixed { if (!is_callable($amount)) { return $amount; } return $amount(); } protected function parseAmountFromString(mixed $amount): mixed { if (!is_string($amount)) { return $amount; } $thousandsSeparator = $this->currency->getThousandsSeparator(); $decimalMark = $this->currency->getDecimalMark(); $amount = str_replace($this->currency->getSymbol(), '', $amount); $amount = preg_replace('/[^\d\\' . $thousandsSeparator . '\\' . $decimalMark . '\-\+]/', '', $amount); $amount = str_replace($this->currency->getThousandsSeparator(), '', $amount); $amount = str_replace($this->currency->getDecimalMark(), '.', $amount); if (preg_match('/^([\-\+])?\d+$/', $amount)) { $amount = (int) $amount; } elseif (preg_match('/^([\-\+])?\d+\.\d+$/', $amount)) { $amount = (float) $amount; } return $amount; } protected function convertAmount(int|float $amount, bool $convert = false): int|float { if (!$convert) { return $amount; } return $amount * $this->currency->getSubunit(); } public static function __callStatic(string $method, array $arguments): Money { $convert = isset($arguments[1]) && is_bool($arguments[1]) && $arguments[1]; return new self($arguments[0], new Currency($method), $convert); } /** * castUsing * * @return class-string */ public static function castUsing(array $arguments): string { return MoneyCast::class; } public static function getLocale(): string { if (empty(static::$locale)) { static::$locale = 'en_GB'; } return static::$locale; } public static function setLocale(?string $locale): void { static::$locale = str_replace('-', '_', (string) $locale); } /** * assertSameCurrency. * * @throws InvalidArgumentException */ protected function assertSameCurrency(Money $other): void { if (!$this->isSameCurrency($other)) { throw new InvalidArgumentException('Different currencies "' . $this->currency . '" and "' . $other->currency . '"'); } } protected function assertRoundingMode(int $mode): void { $modes = [self::ROUND_HALF_UP, self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD]; if (! in_array($mode, $modes)) { throw new OutOfBoundsException('Rounding mode should be ' . implode(' | ', $modes)); } } /** * assertDivisor. * * @throws InvalidArgumentException */ protected function assertDivisor(int|float $divisor): void { if ($divisor == 0) { throw new InvalidArgumentException('Division by zero'); } } public function getAmount(bool $rounded = false): int|float { return $rounded ? $this->getRoundedAmount() : $this->amount; } public function getRoundedAmount(): int|float { return $this->round($this->amount); } public function getValue(): float { return $this->round($this->amount / $this->currency->getSubunit()); } public function getCurrency(): Currency { return $this->currency; } public function isSameCurrency(Money $other): bool { return $this->currency->equals($other->currency); } /** * compare. * * @throws InvalidArgumentException */ public function compare(Money $other): int { $this->assertSameCurrency($other); if ($this->amount < $other->amount) { return -1; } if ($this->amount > $other->amount) { return 1; } return 0; } public function equals(Money $other): bool { return $this->compare($other) == 0; } public function greaterThan(Money $other): bool { return $this->compare($other) == 1; } public function greaterThanOrEqual(Money $other): bool { return $this->compare($other) >= 0; } public function lessThan(Money $other): bool { return $this->compare($other) == -1; } public function lessThanOrEqual(Money $other): bool { return $this->compare($other) <= 0; } public function convert(Currency $currency, int|float $ratio, int $roundingMode = self::ROUND_HALF_UP): Money { $this->currency = $currency; return $this->multiply($ratio, $roundingMode); } public function add(int|float|Money $addend, int $roundingMode = self::ROUND_HALF_UP): Money { if ($addend instanceof Money) { $this->assertSameCurrency($addend); $addend = $addend->getAmount(); } $amount = $this->round($this->amount + $addend, $roundingMode); if ($this->isImmutable()) { return new self($amount, $this->currency); } $this->amount = $amount; return $this; } public function subtract(int|float|Money $subtrahend, int $roundingMode = self::ROUND_HALF_UP): Money { if ($subtrahend instanceof Money) { $this->assertSameCurrency($subtrahend); $subtrahend = $subtrahend->getAmount(); } $amount = $this->round($this->amount - $subtrahend, $roundingMode); if ($this->isImmutable()) { return new self($amount, $this->currency); } $this->amount = $amount; return $this; } public function multiply(int|float $multiplier, int $roundingMode = self::ROUND_HALF_UP): Money { $amount = $this->round($this->amount * $multiplier, $roundingMode); if ($this->isImmutable()) { return new self($amount, $this->currency); } $this->amount = $amount; return $this; } public function divide(int|float $divisor, int $roundingMode = self::ROUND_HALF_UP): Money { $this->assertDivisor($divisor); $amount = $this->round($this->amount / $divisor, $roundingMode); if ($this->isImmutable()) { return new self($amount, $this->currency); } $this->amount = $amount; return $this; } /** * @psalm-suppress ArgumentTypeCoercion */ public function round(int|float $amount, int $mode = self::ROUND_HALF_UP): float { $this->assertRoundingMode($mode); return round($amount, $this->currency->getPrecision(), $mode); } /** * @param array $ratios */ public function allocate(array $ratios): array { $remainder = $this->amount; $results = []; $total = array_sum($ratios); foreach ($ratios as $ratio) { $share = floor($this->amount * $ratio / $total); $results[] = new self($share, $this->currency); $remainder -= $share; } for ($i = 0; $remainder > 0; $i++) { $results[$i]->amount++; $remainder--; } return $results; } public function isZero(): bool { return $this->amount == 0; } public function isPositive(): bool { return $this->amount > 0; } public function isNegative(): bool { return $this->amount < 0; } public function format(): string { $negative = $this->isNegative(); $value = $this->getValue(); $amount = $negative ? -$value : $value; $thousands = $this->currency->getThousandsSeparator(); $decimals = $this->currency->getDecimalMark(); $prefix = $this->currency->getPrefix(); $suffix = $this->currency->getSuffix(); $value = number_format($amount, $this->currency->getPrecision(), $decimals, $thousands); return ($negative ? '-' : '') . $prefix . $value . $suffix; } public function formatSimple(): string { return number_format( $this->getValue(), $this->currency->getPrecision(), $this->currency->getDecimalMark(), $this->currency->getThousandsSeparator() ); } public function formatWithoutZeroes(): string { if ($this->getValue() !== round($this->getValue())) { return $this->format(); } $negative = $this->isNegative(); $value = $this->getValue(); $amount = $negative ? -$value : $value; $thousands = $this->currency->getThousandsSeparator(); $decimals = $this->currency->getDecimalMark(); $prefix = $this->currency->getPrefix(); $suffix = $this->currency->getSuffix(); $value = number_format($amount, 0, $decimals, $thousands); return ($negative ? '-' : '') . $prefix . $value . $suffix; } /** * formatForHumans. * * @throws BadFunctionCallException */ public function formatForHumans(?string $locale = null, ?Closure $callback = null): string { // @codeCoverageIgnoreStart if (! class_exists('\NumberFormatter')) { throw new BadFunctionCallException('Class NumberFormatter not exists. Require ext-intl extension.'); } // @codeCoverageIgnoreEnd $negative = $this->isNegative(); $value = $this->getValue(); $amount = $negative ? -$value : $value; $prefix = $this->currency->getPrefix(); $suffix = $this->currency->getSuffix(); $formatter = new \NumberFormatter($locale ?: static::getLocale(), \NumberFormatter::PADDING_POSITION); $formatter->setSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->currency->getDecimalMark()); $formatter->setSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->currency->getThousandsSeparator()); $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $this->currency->getPrecision()); if (is_callable($callback)) { $callback($formatter); } return ($negative ? '-' : '') . $prefix . $formatter->format($amount) . $suffix; } /** * formatLocale. * * @throws BadFunctionCallException */ public function formatLocale(?string $locale = null, ?Closure $callback = null): string { // @codeCoverageIgnoreStart if (! class_exists('\NumberFormatter')) { throw new BadFunctionCallException('Class NumberFormatter not exists. Require ext-intl extension.'); } // @codeCoverageIgnoreEnd $formatter = new \NumberFormatter($locale ?: static::getLocale(), \NumberFormatter::CURRENCY); $formatter->setSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->currency->getDecimalMark()); $formatter->setSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->currency->getThousandsSeparator()); $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $this->currency->getPrecision()); if (is_callable($callback)) { $callback($formatter); } return $formatter->formatCurrency($this->getValue(), $this->currency->getCurrency()); } public function toArray(): array { return [ 'amount' => $this->amount, 'value' => $this->getValue(), 'currency' => $this->currency, ]; } public function toJson($options = 0): string { return json_encode($this->toArray(), $options); } public function jsonSerialize(): array { return $this->toArray(); } public function render(): string { return $this->format(); } public function immutable(): Money { $this->mutable = false; return new self($this->amount, $this->currency); } public function mutable(): Money { $this->mutable = true; return $this; } public function isMutable(): bool { return $this->mutable === true; } public function isImmutable(): bool { return !$this->isMutable(); } public function __toString(): string { return $this->render(); } }