-
Notifications
You must be signed in to change notification settings - Fork 440
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
[RFC] Add CurrencyInterface and CurrencyLoader #95
Conversation
I have various feelings about this. First of all: you can absolutely create imaginary currency types. There is no limitation on that. The fact that only an ISOCurrency list is available in the repository does not mean you can't. In fact, this is the reason why we introduced AvailableCurrencies: to allow you to provide a custom list of currencies. Secondly: there is a good reason why the Money is not aware of any currency loading at all. It is a value object, there should not be more responsibility in that class. Last, but not least: Currency is a value object too, thus introducing an interface breaks the rule IMHO. Not just because you can implement a custom one, but because the currency should not have anything else, but the data (code) which identifies it. Why do you need an interface then? Lastly: I can hardly see the extra features this PR adds. I mean, what is that you CAN'T do without this PR with the current implementation? Currency auto-loading? That is your responsibility and should not be in the core. And as I proved earlier, you can do it, ISO is not hard coded. For the record: I've been trying to implement something similar earlier (meaning my custom money implementation) with interfaces, loaders and the like, but then I found @mathiasverraes's package and I found it much more better. The point is that I think it tries to be very strict about how money should be handled and reduces extensibility to a minimum level which is fine IMO. |
Thank you @sagikazarmark for your fast feedback! I try to reply to all you comment. Imaginary currency can be created, but there are limitations AFAIK. To add it I can just do I agree that Money library should not know about loading. In fact I think that currency should be always passed in the constructor. The only reason I've introduced this loading login is to allow "Money::CURRENCY" syntax, which is cool and provide backward compatibility, even if it's not SOLID OOP. I think that Currency should be value object (that's why the method So, If I use my own implementation of currency I always need to do something like this: $money = new Money(123, $myCurrency->getCode()); or define my custom Money and Currency with duplicated interface, and for each method do: public function doSomething()
{
$newMoney = $this->wrappedMoney->doSomething();
$currency = $myCurrencyLoader->load($newMoney->getCurrency()->getCode());
return new MyMoney($newMoney, $currency);
} ...or similar. So, in the end. I love too the way this library is written, and that's why I've opened this PR in the first place. I was expecting this kind of feedback. What I say is this: with this PR, the library can be used the same way as before, with All I say obviously don't apply to |
Actually you confused me a little bit.
Isn't it right now?
Can't you do this currently?
That's where I disagree. The point of currency is to provide information about the money value and nothing more. Any further information should be stored elsewhere, where you can actually use the Currency object as the key.
I admit that this is related to the currency. However the point of the Currency object is to let you identify. So this kind of data should be stored elsewhere. (Since you mentioned doctrine: Currency is an association, not an entity). At least this is the case now, but we can argue about it.
Sorry, what? 😄 I have my own doctrine types and extensions for money which work just fine.
That's again something that is out of scope. If I were you, I would create a factory wrapper which contains all your custom knowledge to load currencies and any further custom logic. Also, check the ISOCurrencies, which itself does the currency loading and can check if it is in your custom set. It seems to me that (almost) everything you want is already in the library. That extra which is not should be implemented as custom logic IMHO. |
Mmm probably my explanation was a bit fuzzy :)
You can now both initialize using
Well, for the same reason I think that Currency should be an interface. I mean, If the only information useful from a currency was the code, then it would have become a string property in money. But the class already provide information about CurrencyPair, ISOCurrencies, ... it seem so natural to allow different currencies implementation.
Let's go a bit off-topic. I don't get this. I have many tables with tuple of columns: money and currency. Money is just a "string" and currency is an association to a Currency entity. But to use Doctrine, I cannot just pass "EUR" I should pass the persisted Currency. Are you doing another easier way?
That's definitely not clear, sorry :) As I explained before, with Doctrine I need to pass reference to persisted entities, so the Currency class cannot be used. This mean that if I use Money class as is, I need an Adapter, an abstraction or some EntityListeners to replace the correct instance before persist. This is about doctrine, but I think that if someone save on CSV (shame on you :) is the same.
This is the first thing I have though, but consider this two examples class Factory {
public function createMoney($amount, $currency) {
return new Money($amount, $currency->getCode());
}
public function createCurrency($code) {
return $this->loadOrCreateFromSomewhere($code);
}
}
$eur = $factory->createCurrency("EUR");
$money = $factory->createMoney(123, $eur);
$money->getCurrency()->equals($eur); // ok cool
$eur->customMethod(); // whatever
$money->getCurrency()->customMethod(); // undefined method error Still I have the same problem if not using my custom currencies inside Money, unless I also use a custom money which reimplement all Money method. But this is overkill: this library is small, and if I need to adapt all the classes, then it is better to use another library (or a fork).
Maybe I simply don't see it... maybe you have some example? |
This is only a problem because you want custom currency implementations.
Why not a money interface then? I think currently the approach is to have Currency as a Value Object, thus an interface is not acceptable here. If @mathiasverraes agrees to change that... However, there are still problems with your Currency implementation: for example equals should only accept an object of the same type, not an interface. Typehinting is a perfect solution there, a type check would not be.
ISOCurrencies? Where? I understand it seems easier to have all those information in a Currency object, but it simply does not belong there. Again: Value Object. Short definition: an object representing a simple entity (aka. value in this case). Currency itself (not the object) belongs to the Money (not the object). It really matters if you pay 200 something for an item: dollars or bitcoins? That's why currency belongs to the money. But name? It does not. Imagine the scenario: you are developing a multi language e-commerce site. Do you really want to put the name into the Currency object. Or just the identifier which gives you the proper translation of the currency name. This is just an example, where it is not a good idea, but also shows why it is better to have Currency as a value object.
IIRC @mathiasverraes puts the currency code into the price field and parses the string into a Money object using a DBAL type.
You are overcomplicating things 😄 use Money\AvailableCurrencies;
use Money\Currency;
use Money\Money;
class MoneyFactory
{
private $currencies = [];
public function __construct()
{
// load currencies
// example
$this->currencies['EUR'] = [
'name' => 'Euro',
'reversedName' => 'oruE',
'whatever' => 'anything',
];
}
public function createMoney($amount, $currencyCode)
{
$currency = new Currency($currencyCode);
// optionally check if it is available:
// $currency->isAvailableWithin($this);
return new Money($amount, $currency);
}
/**
* {@inheritdoc}
*/
public function contains(Currency $currency)
{
return isset($this->currencies[$currency->getCode()]);
}
public function getCurrencyName(Currency $currency)
{
if ($currency->isAvailableWithin($this)) {
return $this->currencies[$currency->getCode]['name'];
}
}
} Usage: $factory = new MoneyFactory;
$money = $factory->createMoney(123, 'EUR');
$currencyName = $factory->getCurrencyName($money->getCurrency()); The good thing is that you can inject this factory into any of your custom logic (formatting, storage) and you can use it there. So that these kinds of custom data do not have to travel with the VALUE object, since it is not value, but some kind of metadata. If you are really concerned about separating your code, you can have your own Metadata (AvailableCurrencies with some extensions) class (which handles loading and is injected into your Factory) so that you have the metadata and factory logic at two different places. |
I mean the
You got a point here. If I think of Money as something like a-primitive-type-php-should-have-provided it sounds good: use the Currency as an simple type and leave the rest to the domain (i.e. formatter, available currency contraint, etc...).
So, you are suggesting to have services which provide property and method of currencies, instead of providing property and method along with the currency. The latter seem more convenient in my domain, but maybe is not the way to go... I will think about it more carefully tomorrow... with fresh mind! |
It has nothing to do with the Currency object. It is just there, because it is the most common use case.
Exactly. The implementation itself is up to you. Imagine it like Doctrine's metadata.... |
I would close this for now. |
Ok, as you prefer. By the way @sagikazarmark, some time as passed and I can give you some more feedback. I really loved your approach and used in other situation of my project (e.g. tipical Status[id, name, description]), but still using the CurrencyInterface for Money. Maybe this is just my preference :) Anyway, thank you for your valuable feedback. |
I've shared with you my fork, since I really think this will improve the library usability (i.e. integration is existing systems).
First and foremost: the implementation of the
Money
class is great! Invariant Final Value Object are great for money, and prevent silly update-by-reference errors.The main limitation I found is the
Currency
class. The current limitation are:The solution seems simple at first: replace or wrap (not extend!) the default currency, and provide my decorated version!
But this is not possibile, for multiple reasons:
Currency
implementation, not to abstraction, and so there is no way of providing new behaviour (no Dependency inversion);I know, that you did this to provide Immutable Value Object instances, and this is great when it comes to money, but for Currency, the Immutability condition is not required (as long you don't change the currency code, of course); as in the original Value Object Pattern, value Object only require that object are compared by value, immutability is just a good heuristic.
So, what have I done to the library?
final
implementation ofCurrencyInterface
;CurrencyInterface
;CurrencyLoader
to allocate currency (preconfigured with ISO code for backward compatibility);All tests still pass and it is fully backward compatible. The only drawback I can think of is that if an application use a custom implementation of Currency, it should not change the currency code at runtime if there are Money instances.
To avoid BC I left the following things which I'm not "proud" of:
Currency
+CurrencyInterface
instead ofCurrency
+ISOCurrency
(orDefaultCurrency
);Money::registerLoader
automatically initialize withISOCurrencyLoader
;currencies.php
is included twice inISOCurrencyLoader
andISOCurrencies
;Currency::getName
is deprecated for code, and should be reused to return the ISO name.I hope to hear your feedback, and maybe the feedback from the community.
Related PR: