Skip to content

Conversation

@tigitz
Copy link

@tigitz tigitz commented Oct 21, 2024

Superseeds #94

This is an attempt to solve #30 and #61

First, we need to accept that the Money and Currency objects cannot be final. In reality, they take on different shapes and forms. The current Brick\Money\Money implementation is built around ISO 4217 currencies, but users are trying to adapt it for other purposes, like cryptocurrencies or historical currencies. The reason is simple: the underlying Brick\Money manipulation engine is extremely versatile and would work just as well with those alternative currency types. This is a valid and logical use case.

Secondly, with the main methods for object creation being static, proper dependency injection to configure a global default behavior becomes impossible.

The quickest solution without a major refactor would be to introduce inheritance as an extension point. This allows users to provide custom currency providers tailored to their use cases. The library itself could leverage this to introduce new types of currencies, such as the historic ISO 4217 currencies discussed here: #93 (comment).

It uses inheritance as a point of extension to define the CurrencyProvider used and adapt the code base to rely on a Currency interface instead of the previous Currency (which was actually IsoCurrency).

The static logic for creating Money remains closely tied to IsoCurrencyProvider to prevent a more extensive breaking change.
This approach balances the need for those interested in distinguishing between Money and IsoMoney, while allowing other users to continue using Money as they have since the library's inception, without needing to address this distinction in their context.

You can now do:

class CryptoCurrency implements Currency {

    public function __construct(private string $code, private int $fractionDigits)
    {
        
    }
    public function getCode(): string
    {
        return $this->code;
    }

    public function getDefaultFractionDigits(): int
    {
        return $this->fractionDigits;
    }

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

    public function __toString()
    {
        return $this->code;
    }

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

class CryptoCurrencyProvider implements CurrencyProvider {
    public function getByCode(int|string $code): Currency
    {
        return match ($code) {
            'BTC' => new CryptoCurrency('BTC',  8),
            'ETH' => new CryptoCurrency('ETH', 8),
            default => throw new \InvalidArgumentException("Unknown currency code '$code'"),
        };
    }
}

class CryptoMoney extends \Brick\Money\Money {
    public static function getCurrencyProvider(): CurrencyProvider
    {
        return new CryptoCurrencyProvider();
    }
}

// 'BTC' will be resolved internally to CryptoCurrency thanks to Money#getCurrencyProvider()
$btcs = CryptoMoney::of('0.11223', 'BTC');
// By default Money#getCurrencyProvider() is using IsoCurrencyProvider so 'EUR' will be resolved internally to IsoCurrency
$euros = Money::of('20', 'EUR');

$moneyBag = new MoneyBag();
$moneyBag->add($btcs);
$moneyBag->add($euros);

echo json_encode($moneyBag->getAmounts())."\n";

$exchangeRateProvider = new class implements \Brick\Money\ExchangeRateProvider {
    public function getExchangeRate(string $sourceCurrencyCode, string $targetCurrencyCode): \Brick\Math\BigRational {
        // Mock exchange rate: 1 BTC = 30000 EUR
        return \Brick\Math\BigRational::of('30000');
    }
};

$currencyConverter = new CurrencyConverter($exchangeRateProvider);

// Convert BTC to EUR
$convertedEuros = $currencyConverter->convert($btcs, IsoCurrency::of('EUR'));

echo "Converted BTC to EUR: " . $convertedEuros->getAmount() . " EUR\n";

It has the following breaking changes:

  • 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() for clarity
  • 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

Previously, allowing string|int as currency necessitated objects to identify the corresponding IsoCurrency, creating a tight coupling with a specific IsoCurrencyProvider implementation and a mix of unwanted responsibility for the object. To move towards a more currency-agnostic architecture, it is now advised to resolve the currency beforehand.

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

@c33s
Copy link

c33s commented Aug 23, 2025

would love to see this merged. i like the conecept of onmoon/money of having extending classes like InvoiceIncome which will possible with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants