Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow extending Money with custom currency provider #95

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## [0.11.0](https://github.com/brick/money/releases/tag/0.11.0)
💥 **Breaking changes**

This release tackles significant issues ([#30](https://github.com/brick/money/issues/30), [#61](https://github.com/brick/money/issues/61)) identified by the community to enhance the flexibility of handling custom currencies.

The update supports custom Money types extending the `Money` class, such as CryptoMoney and HistoricalIsoMoney, necessitating careful refactoring of the library's core concepts. For a comprehensive understanding of the design decisions and examples, please refer to the related pull requests.

- `Currency` has been renamed to `IsoCurrency` to better reflect its role in representing ISO 4217 currencies
- `Currency` is now an interface, with `IsoCurrency` serving as its concrete implementation
- `Currency#getCurrencyCode()` is now `Currency#getCode()`
- `UnknownCurrencyException` has been renamed to `UnknownIsoCurrencyException` to align with its specific use for `IsoCurrency` types
- `ISOCurrencyProvider` has been renamed to `IsoCurrencyProvider` to adhere to the [upcoming PSR rules for capitalization of abbreviations](https://github.com/php-fig/per-coding-stylestyle/issues/95)

Previously, allowing `string|int` as currency necessitated objects to identify the corresponding IsoCurrency, creating a tight coupling with a specific IsoCurrencyProvider implementation. To move towards a more currency-agnostic architecture, it is now advised to resolve the currency beforehand to ensure the correct currency is utilized.

- The parameter `$currency` in `Brick\Money\Currency#is()` has been refined to accept only `Brick\Money\Currency`, removing support for `string|int`
- The parameter `$currency` in `Brick\Money\CurrencyConverter#convert()` has been refined to accept only `Brick\Money\Currency`, removing support for `string|int`
- The parameter `$currency` in `Brick\Money\CurrencyConverter#convertToRational()` has been refined to accept only `Brick\Money\Currency`, removing support for `string|int`

## [0.10.0](https://github.com/brick/money/releases/tag/0.10.0) - 2024-10-12

💥 **ISO currency changes**
Expand Down
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,24 +352,44 @@ Writing your own provider is easy: the `ExchangeRateProvider` interface has just

## Custom currencies

Money supports ISO 4217 currencies by default. You can also use custom currencies by creating a `Currency` instance. Let's create a Bitcoin currency:
Money supports ISO 4217 currencies by default. You can also use custom currencies by creating an object implementing a `Currency` interface. Let's create a Bitcoin currency:

```php
use Brick\Money\Currency;
use Brick\Money\Money;

$bitcoin = new Currency(
'XBT', // currency code
0, // numeric currency code, useful when storing monies in a database; set to 0 if unused
'Bitcoin', // currency name
8 // default scale
);
class BtcCurrency implements Currency
{
public function __toString(): string
{
return $this->getCode();
}

public function getCode(): string
{
return 'BTC';
}

public function getDefaultFractionDigits(): int
{
return 8;
}

public function jsonSerialize(): mixed
{
return $this->getCode();
}

public function is(Currency $currency): bool
{
return $currency->getCode() === $this->getCode();
}
}

$bitcoinCurrency = new BtcCurrency();
```

You can now use this Currency instead of a currency code:

```php
$money = Money::of('0.123', $bitcoin); // XBT 0.12300000
$money = Money::of('0.123', $bitcoinCurrency); // BTC 0.12300000
```

## Formatting
Expand Down
7 changes: 6 additions & 1 deletion src/AbstractMoney.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ final public function to(Context $context, RoundingMode $roundingMode = Rounding
final public function getAmounts() : array
{
return [
$this->getCurrency()->getCurrencyCode() => $this->getAmount()
$this->getCurrency()->getCode() => $this->getAmount()
];
}

public static function getCurrencyProvider(): CurrencyProvider
{
return IsoCurrencyProvider::getInstance();
}

/**
* Returns the sign of this money.
*
Expand Down
1 change: 1 addition & 0 deletions src/Context/CashContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Brick\Money\Context;
use Brick\Money\Currency;
use Brick\Money\IsoCurrency;

use Brick\Math\BigDecimal;
use Brick\Math\BigNumber;
Expand Down
1 change: 1 addition & 0 deletions src/Context/CustomContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Brick\Math\RoundingMode;
use Brick\Money\Context;
use Brick\Money\Currency;
use Brick\Money\IsoCurrency;

/**
* Adjusts a number to a custom scale, and optionally step.
Expand Down
1 change: 1 addition & 0 deletions src/Context/DefaultContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Brick\Money\Context;
use Brick\Money\Currency;
use Brick\Money\IsoCurrency;

use Brick\Math\BigDecimal;
use Brick\Math\BigNumber;
Expand Down
174 changes: 4 additions & 170 deletions src/Currency.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,180 +3,14 @@
declare(strict_types=1);

namespace Brick\Money;

use Brick\Money\Exception\UnknownCurrencyException;
use JsonSerializable;
use Stringable;

/**
* A currency. This class is immutable.
*/
final class Currency implements Stringable, JsonSerializable
interface Currency extends Stringable, JsonSerializable
{
/**
* The currency code.
*
* For ISO currencies this will be the 3-letter uppercase ISO 4217 currency code.
* For non ISO currencies no constraints are defined, but the code must be unique across an application, and must
* not conflict with ISO currency codes.
*/
private readonly string $currencyCode;

/**
* The numeric currency code.
*
* For ISO currencies this will be the ISO 4217 numeric currency code, without leading zeros.
* For non ISO currencies no constraints are defined, but the code must be unique across an application, and must
* not conflict with ISO currency codes.
*
* If set to zero, the currency is considered to not have a numeric code.
*
* The numeric code can be useful when storing monies in a database.
*/
private readonly int $numericCode;

/**
* The name of the currency.
*
* For ISO currencies this will be the official English name of the currency.
* For non ISO currencies no constraints are defined.
*/
private readonly string $name;

/**
* The default number of fraction digits (typical scale) used with this currency.
*
* For example, the default number of fraction digits for the Euro is 2, while for the Japanese Yen it is 0.
* This cannot be a negative number.
*/
private readonly int $defaultFractionDigits;

/**
* Class constructor.
*
* @param string $currencyCode The currency code.
* @param int $numericCode The numeric currency code.
* @param string $name The currency name.
* @param int $defaultFractionDigits The default number of fraction digits.
*/
public function __construct(string $currencyCode, int $numericCode, string $name, int $defaultFractionDigits)
{
if ($defaultFractionDigits < 0) {
throw new \InvalidArgumentException('The default fraction digits cannot be less than zero.');
}

$this->currencyCode = $currencyCode;
$this->numericCode = $numericCode;
$this->name = $name;
$this->defaultFractionDigits = $defaultFractionDigits;
}

/**
* Returns a Currency instance matching the given ISO currency code.
*
* @param string|int $currencyCode The 3-letter or numeric ISO 4217 currency code.
*
* @throws UnknownCurrencyException If an unknown currency code is given.
*/
public static function of(string|int $currencyCode) : Currency
{
return ISOCurrencyProvider::getInstance()->getCurrency($currencyCode);
}

/**
* Returns a Currency instance for the given ISO country code.
*
* @param string $countryCode The 2-letter ISO 3166-1 country code.
*
* @return Currency
*
* @throws UnknownCurrencyException If the country code is unknown, or there is no single currency for the country.
*/
public static function ofCountry(string $countryCode) : Currency
{
return ISOCurrencyProvider::getInstance()->getCurrencyForCountry($countryCode);
}

/**
* Returns the currency code.
*
* For ISO currencies this will be the 3-letter uppercase ISO 4217 currency code.
* For non ISO currencies no constraints are defined.
*
* @return string
*/
public function getCurrencyCode() : string
{
return $this->currencyCode;
}

/**
* Returns the numeric currency code.
*
* For ISO currencies this will be the ISO 4217 numeric currency code, without leading zeros.
* For non ISO currencies no constraints are defined.
*
* @return int
*/
public function getNumericCode() : int
{
return $this->numericCode;
}

/**
* Returns the name of the currency.
*
* For ISO currencies this will be the official English name of the currency.
* For non ISO currencies no constraints are defined.
*
* @return string
*/
public function getName() : string
{
return $this->name;
}

/**
* Returns the default number of fraction digits (typical scale) used with this currency.
*
* For example, the default number of fraction digits for the Euro is 2, while for the Japanese Yen it is 0.
*
* @return int
*/
public function getDefaultFractionDigits() : int
{
return $this->defaultFractionDigits;
}

/**
* Returns whether this currency is equal to the given currency.
*
* The currencies are considered equal if their currency codes are equal.
*
* @param Currency|string|int $currency The Currency instance, currency code or numeric currency code.
*
* @return bool
*/
public function is(Currency|string|int $currency) : bool
{
if ($currency instanceof Currency) {
return $this->currencyCode === $currency->currencyCode;
}

return $this->currencyCode === (string) $currency
|| ($this->numericCode !== 0 && $this->numericCode === (int) $currency);
}
public function getCode(): string;

final public function jsonSerialize(): string
{
return $this->currencyCode;
}
public function getDefaultFractionDigits(): int;

/**
* Returns the currency code.
*/
public function __toString() : string
{
return $this->currencyCode;
}
public function is(Currency $currency) : bool;
}
14 changes: 5 additions & 9 deletions src/CurrencyConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __construct(ExchangeRateProvider $exchangeRateProvider)
* Converts the given money to the given currency.
*
* @param MoneyContainer $moneyContainer The Money, RationalMoney or MoneyBag to convert.
* @param Currency|string|int $currency The Currency instance, ISO currency code or ISO numeric currency code.
* @param Currency $currency The Currency instance, ISO currency code or ISO numeric currency code.
* @param Context|null $context A context to create the money in, or null to use the default.
* @param RoundingMode $roundingMode The rounding mode, if necessary.
*
Expand All @@ -44,7 +44,7 @@ public function __construct(ExchangeRateProvider $exchangeRateProvider)
*/
public function convert(
MoneyContainer $moneyContainer,
Currency|string|int $currency,
Currency $currency,
?Context $context = null,
RoundingMode $roundingMode = RoundingMode::UNNECESSARY,
) : Money {
Expand All @@ -57,19 +57,15 @@ public function convert(
* Converts the given money to the given currency, and returns the result as a RationalMoney with no rounding.
*
* @param MoneyContainer $moneyContainer The Money, RationalMoney or MoneyBag to convert.
* @param Currency|string|int $currency The Currency instance, ISO currency code or ISO numeric currency code.
* @param Currency $currency The Currency instance
*
* @return RationalMoney
*
* @throws CurrencyConversionException If the exchange rate is not available.
*/
public function convertToRational(MoneyContainer $moneyContainer, Currency|string|int $currency) : RationalMoney
public function convertToRational(MoneyContainer $moneyContainer, Currency $currency) : RationalMoney
{
if (! $currency instanceof Currency) {
$currency = Currency::of($currency);
}

$currencyCode = $currency->getCurrencyCode();
$currencyCode = $currency->getCode();

$total = BigRational::zero();

Expand Down
15 changes: 15 additions & 0 deletions src/CurrencyProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Brick\Money;

use Brick\Money\Exception\UnknownIsoCurrencyException;

interface CurrencyProvider
{
/**
* @throws UnknownIsoCurrencyException If the currency code is not known.
*/
public function getByCode(string|int $code): Currency;
}
5 changes: 3 additions & 2 deletions src/Exception/MoneyMismatchException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Brick\Money\Exception;

use Brick\Money\Currency;
use Brick\Money\IsoCurrency;

/**
* Exception thrown when a money is not in the expected currency or context.
Expand All @@ -21,8 +22,8 @@ public static function currencyMismatch(Currency $expected, Currency $actual) :
{
return new self(sprintf(
'The monies do not share the same currency: expected %s, got %s.',
$expected->getCurrencyCode(),
$actual->getCurrencyCode()
$expected->getCode(),
$actual->getCode()
));
}

Expand Down
Loading