Open devsi opened 2 years ago
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()
.
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);
}
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!
@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?
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.
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.
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);
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
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:
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);
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!
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."
@puzzledmonkey Unfortunately, this falls under global configuration, which I try to avoid at all costs. ISOCurrencyProvider
is fine though, as it's immutable.
By the way, I forgot about #30 which started the same conversation back in 2020.
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 anew 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.