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

No support for adding CurrencyProviders #61

Open
devsi opened this issue Jun 2, 2022 · 12 comments
Open

No support for adding CurrencyProviders #61

devsi opened this issue Jun 2, 2022 · 12 comments

Comments

@devsi
Copy link

devsi commented Jun 2, 2022

Hi,

Although you do support custom currencies through new Currency, the project does not support the expectation that the Money class may not rely on the ISOCurrencyProvider, and could instead rely on a CurrencyProviderInterface.

I propose making ISOCurrencyProvider an implementation of CurrencyProviderInterface, and setting it to default to maintain the existing functionality, and then enable users to set their own Concrete CurrencyProvider to allow for additional options.

This would cover the case where cryptocurrencies (popularity rising as they are!) can be built using the interface and be validated correctly, instead of having to manually override the string currency with a new Currency() every time. This is for sure necessary as a developer when you want to dynamically load a list of available crypto's. I wouldn't expect the CryptoCurrencyProvider to be a part of this project, but at least the Interface to support additional non standard currencies.

I would be more than happy to pick this work up and submit a PR. I am writing it anyway for my local project but without the reliance on the Interface :)

Thanks.

@BenMorel
Copy link
Member

BenMorel commented Jun 3, 2022

Hi, I don't have a problem with introducing a CurrencyProviderInterface, however I'm reluctant to inject it globally so that it can be used in Money::of() as a default value. I'm against using any kind of global state here.

IMO, if you're using something else than ISO currencies, you should create your Monies from a factory that gets the provider injected, not from Money::of().

@devsi
Copy link
Author

devsi commented Jun 3, 2022

Absolutely I agree. I wouldn't want a secondary provider to come in through Money::of either That makes no sense to me. However coupling the Provider of Currency to the Money object I think would be beneficial, which means there should be an easy way to override the default. As Money::of is the general way of doing this, and there's no state to the Money object (agree there too). I think you're right in that a Factory would be the way to go, but would need a way to create a Money object that uses a different provider without maintaining state of that provider.

Any suggestions as to how such a factory might work? If Money::of doesn't take the provider, but its the way to generate a new object, how does one switch the Provider out?

For my project, i have done the following, any alternative to this?:

function factoryFunc($amount, string $currency, $context, $roundingMode) {
    // getCurrencyFromCode fetches `new Currency` from a Collection of a few different CurrencyProviders
    $currency = $this->getCurrencyFromCode($currency);

    return Money::of($amount, $currency, $context, $roundingMode);
}

@puzzledmonkey
Copy link

puzzledmonkey commented Feb 10, 2023

Hi! Nice library, thanks.

However, version 0.8.0 completely removes HRK from the ISO currencies. While this is correct, we have legacy data in Croatian Kuna, and we use Laravel Nova which interfaces directly to Money::of or Money::ofMinor and does not allow us to hook into this and provide a custom new Currency(...) easily.

Is it possible to implement an extension to ISOCurrencyProvider such as addCurrency(new Currency(...)); which adds the new currency to $currencyData for the singleton? This would allow us (or anyone else) to add currencies at boot time and not have to hook deeper into the system.

Thanks for considering!

@BenMorel
Copy link
Member

@puzzledmonkey I'm generally against adding any kind of global state configuration, as it may have side effects if several parts of your application use brick/money.

What exactly prevents Laravel Nova from using a factory to create Money instances?

@puzzledmonkey
Copy link

Well I can't modify their code, so for now I've just made a small field class that extends their Currency field and overrides the method that uses Money::of.

I understand you're against any kind of global state configuration, but isn't that kind of what the ISOCurrentProvider singleton's currencyData provides? Of course it's up to you, but I just figured it would help people in our case where a legacy currency has been removed but needs supporting for the forseeable future too.

@puzzledmonkey
Copy link

Note: for now I just overrode the Nova Currency field method and add the custom currency in as necessary. However I'm not sure this is the best method either because I'm creating a new Currency object for each call to the field. A global setting would only need to instantiate it once.

@hrvoj3e
Copy link

hrvoj3e commented Jun 18, 2024

So we should use it like this if HRK is needed?

if ('HRK' === $currency) {
    // https://github.com/brick/money/blob/eb7fe4cee92325589dada779000758c0e56b5508/data/iso-currencies.php#L63C5-L63C38
    $currency = new Brick\Money\Currency('HRK', 191, 'Kuna', 2);
} else {
  // leave as is
}

Brick\Money\Money::of($number, $currency);

@puzzledmonkey
Copy link

So we should use it like this if HRK is needed?

if ('HRK' === $currency) {
    // https://github.com/brick/money/blob/eb7fe4cee92325589dada779000758c0e56b5508/data/iso-currencies.php#L63C5-L63C38
    $currency = new Brick\Money\Currency('HRK', 191, 'Kuna', 2);
} else {
  // leave as is
}

Brick\Money\Money::of($number, $currency);

yes that's pretty much exactly the code i used to get this working

@BenMorel
Copy link
Member

So we should use it like this if HRK is needed?

@hrvoj3e Pretty much, yes. I would advise to create a service that gets injected throughout your application, and centralize the currency logic there. It could look like this:

class CurrencyProvider
{
    public function getCurrency(string|int $currencyCode): Currency
    {
        if ('HRK' === $currencyCode) {
            return new Currency('HRK', 191, 'Kuna', 2);
        }

        return Currency::of($currencyCode);
    }
}

and pass the resulting Currency to Money::of().

The library could provide a CurrencyProviderInterface, as suggested by @devsi, but there aren't many ways in which it could be used by brick/money itself without using some kind of global configuration.

The only options I can think of:

  1. add yet another parameter to Money factory methods, which would accept an optional CurrencyProvider:
    class Money
    {
        public function of(
            BigNumber|int|float|string $amount,
            Currency|string|int $currency,
            ?Context $context = null,
            RoundingMode $roundingMode = RoundingMode::UNNECESSARY,
            ?CurrencyProviderInterface $currencyProvider = null,
        ) {
            ...
        }
    );
    Now you must remember to always pass the $currencyProvider argument:
    Money::of(50, 'HRK', currencyProvider: $myCurrencyProvider);
  2. or, in addition to the CurrencyProvider interface, provide a concrete MoneyFactory class, that takes a CurrencyProviderInterface implementation, and has methods to build Money objects that you can use in place of static Money factory methods:
    class MoneyFactory
    {
        public function __construct(
            private CurrencyProviderInterface $currencyProvider,
        ) {
        }
    
        public function of(
            BigNumber|int|float|string $amount,
            Currency|string|int $currency,
            ?Context $context = null,
            RoundingMode $roundingMode = RoundingMode::UNNECESSARY,
        ) {
            if (is_string($currency) || is_int($currency)) {
                $currency = $this->currencyProvider->getCurrency($currency);
            }
    
            return Money::of($amount, $currency, $context, $roundingMode);
        }
    
        // ...additional factory methods
    }
    Now you must always use the MoneyFactory instead of Money::of() and others:
    $moneyFactory->of(50, 'HRK');

If anything, I prefer option 1. At least we don't provide two ways of building Money objects.

In either case, you'll need support from your DI container to always have a CurrencyProvider or a MoneyFactory instance at hand (or use a singleton if you're into this).

Other ideas welcome!

@puzzledmonkey
Copy link

Other ideas welcome!

what about my idea above?

"Is it possible to implement an extension to ISOCurrencyProvider such as addCurrency(new Currency(...)); which adds the new currency to $currencyData for the singleton? This would allow us (or anyone else) to add currencies at boot time and not have to hook deeper into the system."

@BenMorel
Copy link
Member

@puzzledmonkey Unfortunately, this falls under global configuration, which I try to avoid at all costs. ISOCurrencyProvider is fine though, as it's immutable.

@BenMorel
Copy link
Member

By the way, I forgot about #30 which started the same conversation back in 2020.

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

No branches or pull requests

4 participants