A money and currency library for PHP

Overview

Brick\Money

A money and currency library for PHP.

Build Status Coverage Status Latest Stable Version Total Downloads License

Introduction

Working with financial data is a serious matter, and small rounding mistakes in an application may lead to serious consequences in real life. That's why floating-point arithmetic is not suited for monetary calculations.

This library is based on brick/math and handles exact calculations on monies of any size.

Installation

This library is installable via Composer:

composer require brick/money

Requirements

This library requires PHP 7.1 or later.

Although not required, it is recommended that you install the GMP or BCMath extension to speed up calculations.

Project status & release process

While this library is still under development, it is well tested and should be stable enough to use in production environments.

The current releases are numbered 0.x.y. When a non-breaking change is introduced (adding new methods, optimizing existing code, etc.), y is incremented.

When a breaking change is introduced, a new 0.x version cycle is always started.

It is therefore safe to lock your project to a given release cycle, such as 0.5.*.

If you need to upgrade to a newer release cycle, check the release history for a list of changes introduced by each further 0.x.0 version.

Creating a Money

To create a Money, call the of() factory method:

use Brick\Money\Money;

$money = Money::of(50, 'USD'); // USD 50.00
$money = Money::of('19.9', 'USD'); // USD 19.90

Alternatively, you can create a Money from a number of "minor units" (cents), using the ofMinor() method:

use Brick\Money\Money;

$money = Money::ofMinor(1234, 'USD'); // USD 12.34

Basic operations

Money is an immutable class: its value never changes, so it can be safely passed around. All operations on a Money therefore return a new instance:

use Brick\Money\Money;

$money = Money::of(50, 'USD');

echo $money->plus('4.99'); // USD 54.99
echo $money->minus(1); // USD 49.00
echo $money->multipliedBy('1.999'); // USD 99.95
echo $money->dividedBy(4); // USD 12.50

You can add and subtract Money instances as well:

use Brick\Money\Money;

$cost = Money::of(25, 'USD');
$shipping = Money::of('4.99', 'USD');
$discount = Money::of('2.50', 'USD');

echo $cost->plus($shipping)->minus($discount); // USD 27.49

If the two Money instances are not of the same currency, an exception is thrown:

use Brick\Money\Money;

$a = Money::of(1, 'USD');
$b = Money::of(1, 'EUR');

$a->plus($b); // MoneyMismatchException

If the result needs rounding, a rounding mode must be passed as second parameter, or an exception is thrown:

use Brick\Money\Money;
use Brick\Math\RoundingMode;

$money = Money::of(50, 'USD');

$money->plus('0.999'); // RoundingNecessaryException
$money->plus('0.999', RoundingMode::DOWN); // USD 50.99

$money->minus('0.999'); // RoundingNecessaryException
$money->minus('0.999', RoundingMode::UP); // USD 49.01

$money->multipliedBy('1.2345'); // RoundingNecessaryException
$money->multipliedBy('1.2345', RoundingMode::DOWN); // USD 61.72

$money->dividedBy(3); // RoundingNecessaryException
$money->dividedBy(3, RoundingMode::UP); // USD 16.67

Money contexts

By default, monies have the official scale for the currency, as defined by the ISO 4217 standard (for example, EUR and USD have 2 decimal places, while JPY has 0) and increment by steps of 1 minor unit (cent); they internally use what is called the DefaultContext. You can change this behaviour by providing a Context instance. All operations on Money return another Money with the same context. Each context targets a particular use case:

Cash rounding

Some currencies do not allow the same increments for cash and cashless payments. For example, CHF (Swiss Franc) has 2 fraction digits and allows increments of 0.01 CHF, but Switzerland does not have coins of less than 5 cents, or 0.05 CHF.

You can deal with such monies using CashContext:

use Brick\Money\Money;
use Brick\Money\Context\CashContext;
use Brick\Math\RoundingMode;

$money = Money::of(10, 'CHF', new CashContext(5)); // CHF 10.00
$money->dividedBy(3, RoundingMode::DOWN); // CHF 3.30
$money->dividedBy(3, RoundingMode::UP); // CHF 3.35

Custom scale

You can use custom scale monies by providing a CustomContext:

use Brick\Money\Money;
use Brick\Money\Context\CustomContext;
use Brick\Math\RoundingMode;

$money = Money::of(10, 'USD', new CustomContext(4)); // USD 10.0000
$money->dividedBy(7, RoundingMode::UP); // USD 1.4286

Auto scale

If you need monies that adjust their scale to fit the operation result, then AutoContext is for you:

use Brick\Money\Money;
use Brick\Money\Context\AutoContext;

$money = Money::of('1.10', 'USD', new AutoContext()); // USD 1.1
$money->multipliedBy('2.5'); // USD 2.75
$money->dividedBy(8); // USD 0.1375

Note that it is not advised to use AutoContext to represent an intermediate calculation result: in particular, it cannot represent the result of all divisions, as some of them may lead to an infinite repeating decimal, which would throw an exception. For these use cases, RationalMoney is what you need. Head on to the next section!

Advanced calculations

You may occasionally need to chain several operations on a Money, and only apply a rounding mode on the very last step; if you applied a rounding mode on every single operation, you might end up with a different result. This is where RationalMoney comes into play. This class internally stores the amount as a rational number (a fraction). You can create a RationalMoney from a Money, and conversely:

use Brick\Money\Money;
use Brick\Math\RoundingMode;

$money = Money::of('9.5', 'EUR') // EUR 9.50
  ->toRational() // EUR 950/100
  ->dividedBy(3) // EUR 950/300
  ->plus('17.795') // EUR 6288500/300000
  ->multipliedBy('1.196') // EUR 7521046000/300000000
  ->to($money->getContext(), RoundingMode::DOWN) // EUR 25.07

As you can see, the intermediate results are represented as fractions, and no rounding is ever performed. The final to() method converts it to a Money, applying a context and a rounding mode if necessary. Most of the time you want the result in the same context as the original Money, which is what the example above does. But you can really apply any context:

...
  ->to(new CustomContext(8), RoundingMode::UP); // EUR 25.07015334

Note: as you can see in the example above, the numbers in the fractions can quickly get very large. This is usually not a problem—there is no hard limit on the number of digits involved in the calculations—but if necessary, you can simplify the fraction at any time, without affecting the actual monetary value:

...
  ->multipliedBy('1.196') // EUR 7521046000/300000000
  ->simplified() // EUR 3760523/150000

Money allocation

You can easily split a Money into a number of parts:

use Brick\Money\Money;

$money = Money::of(100, 'USD');
[$a, $b, $c] = $money->split(3); // USD 33.34, USD 33.33, USD 33.33

You can also allocate a Money according to a list of ratios. Say you want to distribute a profit of 987.65 CHF to 3 shareholders, having shares of 48%, 41% and 11% of a company:

use Brick\Money\Money;

$profit = Money::of('987.65', 'CHF');
[$a, $b, $c] = $profit->allocate(48, 41, 11); // CHF 474.08, CHF 404.93, CHF 108.64

It plays well with cash roundings, too:

use Brick\Money\Money;
use Brick\Money\Context\CashContext;

$profit = Money::of('987.65', 'CHF', new CashContext(5));
[$a, $b, $c] = $profit->allocate(48, 41, 11); // CHF 474.10, CHF 404.95, CHF 108.60

Note that the ratios can be any (non-negative) integer values and do not need to add up to 100.

When the allocation yields a remainder, both split() and allocate() spread it on the first monies in the list, until the total adds up to the original Money. This is the algorithm suggested by Martin Fowler in his book Patterns of Enterprise Application Architecture. You can see that in the first example, where the first money gets 33.34 dollars while the others get 33.33 dollars.

Money bags (mixed currencies)

You may sometimes need to add monies in different currencies together. MoneyBag comes in handy for this:

use Brick\Money\Money;
use Brick\Money\MoneyBag;

$eur = Money::of('12.34', 'EUR');
$jpy = Money::of(123, 'JPY');

$moneyBag = new MoneyBag();
$moneyBag->add($eur);
$moneyBag->add($jpy);

You can add any kind of money to a MoneyBag: a Money, a RationalMoney, or even another MoneyBag.

Note that unlike other classes, MoneyBag is mutable: its value changes when you call add() or subtract().

What can you do with a MoneyBag? Well, you can convert it to a Money in the currency of your choice, using a CurrencyConverter. Keep reading!

Currency conversion

This library ships with a CurrencyConverter that can convert any kind of money (Money, RationalMoney or MoneyBag) to a Money in another currency:

use Brick\Money\CurrencyConverter;

$exchangeRateProvider = ...;
$converter = new CurrencyConverter($exchangeRateProvider); // optionally provide a Context here

$money = Money::of('50', 'USD');
$converter->convert($money, 'EUR', RoundingMode::DOWN);

The converter performs the most precise calculation possible, internally representing the result as a rational number until the very last step.

To use the currency converter, you need an ExchangeRateProvider. Several implementations are provided, among which:

ConfigurableProvider

This provider starts with a blank state, and allows you to add exchange rates manually:

use Brick\Money\ExchangeRateProvider\ConfigurableProvider;

$provider = new ConfigurableProvider();
$provider->setExchangeRate('EUR', 'USD', '1.0987');
$provider->setExchangeRate('USD', 'EUR', '0.9123');

PDOProvider

This provider reads exchange rates from a database table:

use Brick\Money\ExchangeRateProvider\PDOProvider;
use Brick\Money\ExchangeRateProvider\PDOProviderConfiguration;

$pdo = new \PDO(...);

$configuration = new PDOProviderConfiguration;
$configuration->tableName = 'exchange_rates';
$configuration->sourceCurrencyColumnName = 'source_currency_code';
$configuration->targetCurrencyColumnName = 'target_currency_code';
$configuration->exchangeRateColumnName = 'exchange_rate';

$provider = new PDOProvider($pdo, $configuration);

PDOProvider also supports fixed source or target currency, and dynamic WHERE conditions. Check the PDOProviderConfiguration class for more information.

BaseCurrencyProvider

This provider builds on top of another exchange rate provider, for the quite common case where all your available exchange rates are relative to a single currency. For example, the exchange rates provided by the European Central Bank are all relative to EUR. You can use them directly to convert EUR to USD, but not USD to EUR, let alone USD to GBP.

This provider will combine exchange rates to get the expected result:

use Brick\Money\ExchangeRateProvider\ConfigurableProvider;
use Brick\Money\ExchangeRateProvider\BaseCurrencyProvider;

$provider = new ConfigurableProvider();
$provider->setExchangeRate('EUR', 'USD', '1.1');
$provider->setExchangeRate('EUR', 'GBP', '0.9');

$provider = new BaseCurrencyProvider($provider, 'EUR');
$provider->getExchangeRate('EUR', 'USD'); // 1.1
$provider->getExchangeRate('USD', 'EUR'); // 10/11
$provider->getExchangeRate('GBP', 'USD'); // 11/9

Notice that exchange rate providers can return rational numbers!

Write your own provider

Writing your own provider is easy: the ExchangeRateProvider interface has just one method, getExchangeRate(), that takes the currency codes and returns a number.

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:

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
);

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

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

Formatting

Formatting requires the intl extension.

Money objects can be formatted according to a given locale:

$money = Money::of(5000, 'USD');
echo $money->formatTo('en_US'); // $5,000.00
echo $money->formatTo('fr_FR'); // 5 000,00 $US

Alternatively, you can format Money objects with your own instance of NumberFormatter, which gives you room for customization:

$formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY);
$formatter->setSymbol(\NumberFormatter::CURRENCY_SYMBOL, 'US$');
$formatter->setSymbol(\NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, '·');
$formatter->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 2);

$money = Money::of(5000, 'USD');
echo $money->formatWith($formatter); // US$5·000.00

Important note: because formatting is performed using NumberFormatter, the amount is converted to floating point in the process; so discrepancies can appear when formatting very large monetary values.

Storing the monies in the database

Persisting the amount

  • As an integer: in many applications, monies are only ever used with their default scale (e.g. 2 decimal places for USD, 0 for JPY). In this case, the best practice is to store minor units (cents) as an integer field:

    $integerAmount = $money->getMinorAmount()->toInt();

    And later retrieve it as:

    Money::ofMinor($integerAmount, $currencyCode);

    This approach works well with all currencies, without having to worry about the scale. You only have to worry about not overflowing an integer (which would throw an exception), but this is unlikely to happen unless you're dealing with huge amounts of money.

  • As a decimal: for most other cases, storing the amount string as a decimal type is advised:

    $decimalAmount = (string) $money->getAmount();

    And later retrieve it as:

    Money::of($decimalAmount, $currencyCode);

Persisting the currency

  • As a string: if you only deal with ISO currencies, or custom currencies having a 3-letter currency code, you can store the currency in a CHAR(3). Otherwise, you'll most likely need a VARCHAR. You may also use an ENUM if your application uses a fixed list of currencies.

    $currencyCode = $money->getCurrency()->getCurrencyCode();

    When retrieving the currency: you can use ISO currency codes directly in Money::of() and Money::ofMinor(). For custom currencies, you'll need to convert them to Currency instances first.

  • As an integer: if you only deal with ISO currencies, or custom currencies with a numeric code, you may store the currency code as an integer:

    $numericCode = $money->getCurrency()->getNumericCode();

    When retrieving the currency: you can use numeric codes of ISO currencies directly in Money::of() and Money::ofMinor(). For custom currencies, you'll need to convert them to Currency instances first.

  • Hardcoded: if your application only ever deals with one currency, you may very well hardcode the currency code and not store it in your database at all.

Using an ORM

If you're using an ORM such as Doctrine, it is advised to store the amount and currency separately, and perform conversion in the getters/setters:

class Entity
{
    private int $price;
    private string $currencyCode;

    public function getPrice() : Money
    {
        return Money::ofMinor($this->price, $this->currencyCode);
    }

    public function setPrice(Money $price) : void
    {
        $this->price = $price->getMinorAmount()->toInt();
        $this->currencyCode = $price->getCurrency()->getCurrencyCode();
    }
}

FAQ

How does this project compare with moneyphp/money?

Please see this discussion.

brick/money for enterprise

Available as part of the Tidelift Subscription.

The maintainers of brick/money and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. Learn more.

Comments
  • There are equals method?

    There are equals method?

    I need a simple bool method what answer me are the two money object completely equal (amount and currency) or not. isEqualTo looks like this method, but it throws MoneyMismatchException exception if currencies are different. What I should use for this problem?

    opened by solodkiy 57
  • Remove final definition from classes

    Remove final definition from classes

    Good afternoon,

    I was wondering if we could safely drop the final definition from the classes within the package. I understand the value of defining certain classes as such, but I could see several valid use cases where extending the Money class, for example, would make sense.

    As a current use case, I'd like to do this in a Laravel app to add the Arrayable and JsonSerializable interfaces to the Money class. I don't think this should be a part of this package, so it makes more sense to permit extension by package consumers.

    Without the ability to extend, I'll have to throw together a factory and forward calls using magic methods. Yuk. Or make a fork. Also seems unnecessary.

    If there is a particular reason why the classes are defined as final in the package, I'd appreciate a quick explanation so that I can decide how to proceed from there.

    Thanks for your time.

    on hold 
    opened by steven-fox 33
  • Scales and Money classes

    Scales and Money classes

    Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

    Currently, a single class, Money, allows to work with default scales (1.50 USD) but also with arbitrary scales (1.123456 USD). I like this flexibility, but it comes at a price:

    • You never know what kind of money you're dealing with. Say you write a function that accepts a Money and performs some basic calculations on it:

      function addDeliveryFee(Money $orderTotal) : Money
      {
          $deliveryFee = Money::of('1.90', 'USD');
          return $orderTotal->plus($deliveryFee);
      }
      

      You're expecting that everyone will call this function with a typical money like 1.23 USD, but suddenly someone calls your function with 1.2345 USD. Because Money retains the left operand scale by default, your function will return 3.1345 USD. Probably not what you want. You may want to fail in some way here, such as throwing an exception.

      Sure, you could check $money->getAmount()->getScale() , but doing so in every function that accepts a Money? Let's be honest: you don't want to do that.

    • I personally store all my (default scale) monies in the database as integers, representing "minor units" (cents). For example, 1.23 USD is stored as 123, while 500 JPY is just stored as 500. To do that, I call $money->getAmountMinor() that gives me 123 for 1.23 USD. The problem is, by doing so I'm assuming that I'm dealing with a standard scale money; if for any reason I get a high precision money such as 1.2345 USD, getAmountMinor() will return 12345, and if I blindly store this in my database to later retrieve it as a standard scale money, I would end up with 123.45 USD! Same problem as above then, we may need a way to enforce a default scale money at some point for safety reasons.

    To be honest, this kind of problem never occurred to me so far, as I have full control over my code from A to Z, and usually only deal with default scale monies. Still, it has always made me feel uncomfortable, and I'm afraid that it could lead to potential issues in third-party libraries dealing with monies. Should we expect them to do their own scale check, or to trust that the caller will give a money with the correct scale? Or should we provide a way to enforce at least default scale monies?

    I can see 2 options here:

    • leave it as it is, and maybe at least provide a convenience method such as isDefaultScale() so that methods not trusting the caller may more easily do their own check;

    • use type safety to have a way to ensure that you're dealing with a standard scale money. In this scenario, we would have 2 different classes for default scale monies and arbitrary scale monies, which could be implemented in many different ways:

      • use 2 completely distinct classes such as Money (default scale) and BigMoney (arbitrary scale). This is the approach used by Joda Money (Java); in their implementation, the 2 classes are not fully interchangeable. For example, while BigMoney::plus() accepts either a Money or a BigMoney, Money::plus() only accepts another Money.
      • keep Money as it is, and add a subclass called something like DefaultMoney (hate the name). If you instantiate a Money with a default scale, it would return a DefaultMoney; this way, if you write a function that expects a Money, it will accept a DefaultMoney, but if it expects a DefaultMoney, it will not accept a Money.
      • a mix of both: use separate Money/BigMoney classes as above, but have them implement a common interface. This is similar to point 1 because we would have 2 separate classes that do not extend each other, but similar to point 2 because we would have a fully shared interface so that Money::plus() and BigMoney::plus() will accept both a Money and a BigMoney (is this necessary, after all?)

    When I started this library, I didn't like Joda's Money/BigMoney approach very much. I believed in a generic Money class that would handle all use cases. Now that I have it somehow, I realize that there might be more drawbacks than advantages.

    In most projects, needless to say that we'll be using default scale monies way more often than arbitrary scale ones. So it does make sense to have a special case (class) for them. This way, one can be sure when dealing with a Money, that it's always the default scale.

    As a final note, I also checked the approach introduced by the new Java Money API (JSR-354) and its reference implementation. It made me curious as they use a common interface (MonetaryAmount) for all monies. I thought this was good, until I realized that it is so generic that implementations have to store the context in the Money class itself. The consequence of this is that when you accept a Money, you get not only a value and a currency, but also how it's supposed to be rounded. I don't believe this is good, as I prefer to receive only a monetary amount, and decide how I will perform the calculations and roundings in every operation I write. It's also worth noting that for now, Joda Money, which is very popular in the Java world, doesn't implement JSR-354; this may change as Java 9 (the first version to include the Money API) is not out yet, but last time I checked, the lead developer was not keen on the spec.

    Note that if I were to follow the Money/BigMoney path, this would affect these two other issues:

    • #4 Money contexts:
      • Money::plus() and Money::minus() would not require a MoneyContext anymore, because the constant scale would guarantee that no rounding is necessary
      • Money::multipliedBy() and Money::dividedBy() would still require a rounding mode, but not a full MoneyContext
    • #6 Implementation / performance: we could consider implementing Money using an integer, while BigMoney would still use BigDecimal.

    I'm really looking forward to your opinion on this one, which I consider the most important issue of all.

    opened by BenMorel 21
  • Money contexts

    Money contexts

    Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

    Monies can have different scales: for example USD uses 2 decimals by default, but some apps may require additional precision and be able to represent 1.123456 USD. Additionally, some arithmetic operation on monies may require rounding: for example dividing 1.00 USD by 3.

    When performing operations on monies, a MoneyContext controls the scale and the rounding. The available implementations are:

    • DefaultContext: adjusts the scale to the currency's default fraction digits (2 for EUR/USD/GBP/…, 0 for JPY), using rounding if necessary.
    • FixedContext: adjusts the scale to a given value, using rounding if necessary.
    • RetainContext: uses the scale of the left operand. This is the default context for arithmetic operations: plus(), minus(), multipliedBy(), dividedBy(). For example, 0.1234 USD + 1.00 USD equals 1.1234 USD, while 1.00 USD + 0.1234 USD uses rounding to fit the result in 2 decimals.
    • ExactContext: tries to adjust the scale to fit the result. For example, 0.99 USD / 2 would equal 0.495 USD, while 1.00 USD / 3 would throw an exception.

    The first 3 contexts require a MoneyRounding implementation:

    • MathRounding uses brick/math's rounding modes directly
    • CashRounding uses rounding modes and steps, for example every 5 cents: 0.05, 0.10, 0.15, etc.

    Contexts are very flexible and powerful, but it's a lot to digest when you're new to the library; even when you're comfortable with them, they're currently a pain to deal with.

    For example, when dividing a money, you will most probably need rounding (unless you're really lucky that the results fits in the scale of your Money).

    To use rounding, you need to pass a MoneyContext:

    use Brick\Math\RoundingMode;
    use Brick\Money\MoneyRounding\MathRounding;
    use Brick\Money\MoneyContext\RetainContext;
    use Brick\Money\Money;
    
    $rounding = new MathRounding(RoundingMode::UP);
    $context = new RetainContext($rounding);
    echo Money::of('10.00', 'USD')->dividedBy(3, $context); // 3.34
    

    This is too long. We need a shortcut for these simple use cases that we use daily.

    Maybe these methods should accept not only a MoneyContext , but also a MoneyRounding, and even a RoundingMode constant directly.

    This means that all three statements below would be equivalent:

    $money->dividedBy(3, RoundingMode::UP);
    $money->dividedBy(3, new MathRounding(RoundingMode::UP));
    $money->dividedBy(3, new RetainContext(new MathRounding(RoundingMode::UP)));
    

    So our example above could be simplified to:

    use Brick\Math\RoundingMode;
    use Brick\Money\Money;
    
    echo Money::of('10.00', 'USD')->dividedBy(3, RoundingMode::UP); // 3.34
    

    We may then argue that the constructors of the MoneyContext classes that accept a MoneyRounding should also accept a RoundingMode constant directly for convenience.

    I hope that this could make it easy to use and less verbose for everyday use cases, while still allowing for more complex examples.

    Thoughts on this? Alternative ideas? Are you happy with the overall context thing?

    Note that depending on the implementation chosen in #3 Scales and Money classes, using contexts could be avoided entirely for standard (default scale) monies, which would only use rounding modes.

    opened by BenMorel 16
  • Implementation / performance

    Implementation / performance

    Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

    The library is fully based on Brick\Math's BigDecimal. This is good, as it uses precise arithmetic. However, I feel like it's a bit overkill when you typically deal with monies of reasonable size, that could be easily represented with integers: you can fit up to ~20 million USD with 2 decimals on 32-bit, and an unfathomable amount on 64-bit.

    It's interesting to note that the Java Money reference implementation provides two classes: Money based on BigDecimal, and FastMoney based on long.

    Sure, we don't have the same speed requirements as a Java application: we won't use this library for stuff like real-time trading, but using GMP or BCMath under the hood when you're just adding/subtracting everyday monies is questionable.

    Note that even if we had an integer-based implementation, integer arithmetic could be used only for the simplest calculations: we'd still need to use BigDecimal for things like division, some multiplication, and probably rounding.

    My current view on this is: if the library is kept as it is today (a single Money class), I don't think it's worth having two implementations. However, as suggested in #3 Scales and Money classes, if we followed the Money/BigMoney route, we could consider going with int for Money, and with BigDecimal for BigMoney.

    opened by BenMorel 13
  • Scales & contexts

    Scales & contexts

    This is kind of a merger of other issues, in particular #3 (Scales and Money classes) and #4 (Money contexts), that happen to be tightly related.

    I figured we've reached a point where we have a common view of a few things, and several conflicting ideas on others. I'll try to summarize the current status of the brainstorming.

    I hope this issue will help us find some common ground.

    What we (seem to) agree on

    • There should be a single Money class (no Money/BigMoney split)
    • A Money is composed of a BigDecimal and a Currency
      • the BigDecimal can have any scale, as required by the application
      • the Currency is composed of a currency code, and a default scale; this scale can be used to provide a default scale in factory methods, and dictates how some methods such as getAmountMinor() behave, independently of the scale of the Money itself
    • A Money should never embed a rounding mode, the caller of an operation on a Money should always be in control of rounding
    • Operations on a Money should be able to round by steps, to support cash increments of 1.00 units (step 100, CZK) or 0.05 units (step 5, CHF); steps for ISO currencies will not be hardcoded in the library, and will be left up to the developer
    • Some use cases can require several consecutive operations on monies, where you do not want to apply any kind of scaling or rounding until the very last step; we should therefore provide a calculator based on BigRational, that doesn't take any scale, step or rounding mode until the getResult() method is called

    What we disagree on

    Actually there is just one main friction point: operations. Should all operations require a full context (target scale, step, and rounding mode), or should a Money instance dictate what the result scale & step are?

    I tried to summarize your points of view here, feel free to correct me if I'm wrong:

    • @jkuchar and I like the idea of storing the step in the Money itself. All operations on a Money would yield another Money with the same scale & step capabilities:

      $money = Money::of(50, 'USD'); // USD 50.00
      $money = $money->dividedBy(3, RoundingMode::HALF_UP); // USD 16.67
      
      $money = Money::of(50, 'CZK', /* step */ 100); // CZK 50.00
      $money = $money->dividedBy(3, RoundingMode::DOWN); // CZK 16.00
      $money = $money->dividedBy(3, RoundingMode::DOWN); // CZK 5.00
      

      The rationale behind this is that usually, an application deals with a fixed scale for a given currency (e.g. the currency's default scale for an online shop, or a higher scale for a Forex trading website), and the need to juggle scales in the middle of a series of operations seems very unusual. I personally feel like the need for a sudden change of scale might be an indicator that you're trying to do something that would be a better fit for a BigRational-based calculator.

      Note that we could allow an optional context to be provided, to allow overriding the current scale & step. This would just not be mandatory.

      Critics include the fact that plus() may throw an exception when adding an amount whose scale is larger than that of the left operand, instead of returning a Money with an adjusted scale (as BigDecimal would do), and that the result depends on the order of the operands (USD 1.20 + USD 0.1 = USD 1.30 while USD 0.1 + USD 1.20 = USD 1.3, and USD 1.21 + USD 0.1 = USD 1.31 while USD 0.1 + USD 1.21 = Exception). I replied here.

    • @jiripudil is not against this behaviour, but suggests that we'd throw an exception when adding together monies of different scales & steps. I replied here.

    • Finally, @VasekPurchart sees Money as an "anemic" value object, that ideally would not contain any operations. He's not fully against having operations on Money though, but in this case suggests that all operations would have to provide the full context: scale, step, and rounding mode. (Note: this is pretty much what we have today and what I was trying to get away from).

    What others are doing

    This is just an overview of what I could investigate in a reasonable timeframe. If you know other libraries that you think deserve to be mentioned, please let me know.

    moneyphp (PHP)

    This PHP library offers a single class, Money, that only stores amounts in integer form, so in "minor units". Currencies are only defined by a currency code. No scale is involved. Multiplication and division take an optional rounding mode, defaulting to HALF_UP.

    Joda Money (Java)

    This popular library offers two implementations, Money and BigMoney:

    • Money always has the default scale for the currency. The result of an operation always has the same scale: plus() must add an amount that is compatible with this scale or you get an exception, and dividedBy() must provide a rounding mode.
    • BigMoney has a flexible scale. BigMoney.plus() adjusts the scale of the result, effectively acting like a BigDecimal with a Currency: USD 25.95 + 3.021 = USD 28.971. BigMoney.dividedBy() returns a BigMoney with the same scale as the left operand: USD 1.00 / 3, rounded DOWN = USD 0.33. You cannot choose the scale of the result.

    Java 9 Money API (JSR 354) (Java)

    This is the new money interface that is now part of Java from version 9 onwards. Java 9 is due to be released tomorrow, 21 September 2017; you can hardly dream of a fresher API! It's been created by a team of experts, several of them working for Credit Suisse.

    This API defines a MonetaryAmount interface that Money classes must implement. MonetaryAmount instances embed a MonetaryContext that defines "the numeric capabilities, e.g. the supported precision and maximal scale, as well as the common implementation flavor."

    According to the source code documentation, operations like add(), multiply() and divide() take a single argument, the operand. The result's scale is adjusted just like a BigDecimal would do, but an exception can be thrown if the scale of the result exceeds the max scale defined by the context.

    The reference implementation, Moneta, provides several classes:

    • Money, based on BigDecimal
    • FastMoney, based on long
    • RoundedMoney, based on BigDecimal like Money, but additionally embedding a custom rounding implementation, that is applied to every operation result. The resulting monies, in turn, embed this rounding for further calculations.

    I gave Moneta a try:

    CurrencyUnit usDollar = Monetary.getCurrency("USD");
    BigDecimal amount = new BigDecimal("100.00");
    System.out.println(Money.of(amount, usDollar).divide(3));
    System.out.println(FastMoney.of(amount, usDollar).divide(3));
    
    INFO: Using custom MathContext: precision=256, roundingMode=HALF_EVEN
    USD 33.33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
    USD 33.33333
    

    These defaults are plain nonsense if you ask me.

    Python Money Class (Python)

    Monies have the scale of the number they were instantiated with. Adding two monies of different scales will return a Money with the maximum of the two scales. Dividing a Money will return a number with a fixed number of decimals, rounded:

    >>> m = Money("100.0", "USD");
    >>> m
    USD 100.0
    >>> m + Money("1.23", "USD");
    USD 101.23
    >>> m/3
    USD 33.33333333333333333333333333
    

    Ping @martinknor, @fortis and @fprochazka again, now is the time to make your voice heard before the first beta release!

    opened by BenMorel 11
  • Custom currencies

    Custom currencies

    Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

    The library ships with ISO currencies, but allows you to add custom currencies (bitcoins, etc.). This is done through a class called DefaultCurrencyProvider; Money uses it to resolve a currency code such as USD to a Currency instance, to get its default scale among other things.

    My issue with the current implementation is that it's a singleton: if your code adds a custom currency, it affects third-party code using Money as well, and vice versa. ISO currencies can't be overridden, but conflicts between custom currencies are possible.

    The only way I can see around this, would be to not allow currency codes in Money factory methods, and force a Currency instance to be provided. This used to be this way, but this was very unfriendly to use. You want to be able to write:

    Money::of('9.90', 'USD');
    

    and not:

    Money::of('9.90', ISOCurrency::of('USD'));
    

    So at some point, you have to resolve the currency code from somewhere, and I could not find a better idea than making this a global, singleton configuration.

    Now that I'm thinking about it, another option could be to allow a currency code string to be provided for ISO currencies only; custom currencies would need to be provided as a Currency object. This would remove the currency provider bloat entirely. Thoughts?

    Side question: the Currency object has this thing called the default fraction digits (default scale, for example 2 for USD/EUR/etc.); do cryptocurrencies have a somewhat official, default scale like traditional currencies?

    opened by BenMorel 11
  • Currency conversion precision

    Currency conversion precision

    Hi!

    Coming over from other packages, I've got to say this is definitely what I needed for this use case which deals with multiple currencies and down-to-the-last-cent precision work needed. However, one thing that bugs me is that I lose precision using the CurrencyConverter class. In the class, you are using BigRational to store and represent the money before returning it as a new Money instance.

    Would it be possible instead to return it as as RationalMoney or even BigRational instead of a Money instance to be able to keep precision? Maybe a boolean parameter? This gives developers freedom to apply a context and/or rounding even after conversion to another currency.

    My use case is I store the rational equivalents (as a string with it's currency through RationalMoney, maybe a different or query efficient approach can be suggested?) on the database to keep it precise, only using context and rounding upon display. So if there is another subsequent operation on the money object, precision will not be lost.

    opened by kmichailg 10
  • Formatting the Number part in toString or add formatting functions

    Formatting the Number part in toString or add formatting functions

    Hi, Is there a way to format string echo-ed? I meanis echo Money::of(5000, 'TZS') should display TZS 5,000.00 instead of current TZS 5000.00. It makes reading big number easier.

    PHP have already number formatting function and it wouldn't be very hard to add its.

    I'm not sure if every currency have corresponding separator or even if there is such a thing as universal separator. If indeed there is separator per currency we can check the currency and apply number_format accordingly. If not, then we can add a format($separator=',') function to money class that works the same as toString() but formats the number part of the result.

    What do you think?

    opened by mtangoo 10
  • Skip intl tests if extenstion is not installed

    Skip intl tests if extenstion is not installed

    Resolve test errors if intl extenstion is not installed

    
    1) Brick\Money\Tests\MoneyTest::testFormatTo with data set #0 (array('1.23', 'USD'), 'en_US', false, '$1.23')
    Error: Class 'NumberFormatter' not found
    
    /home/doc/projects/_opensource/money/src/Money.php:686
    /home/doc/projects/_opensource/money/tests/MoneyTest.php:900
    
    opened by solodkiy 7
  • Issues with comparison - returns false although both are the same

    Issues with comparison - returns false although both are the same

    Hey Ben,

    I'm running into an issue where I'm unsure if we're looking at a bug. When comparing a Money to an int, it returns false as in it's not the same, although it is.

    I have these dumps in my phpunit tests:

    # $this->min_bid = Money obj of 0.06 USD
    # $this->optimized_bid = int 6
    
    dump($this->min_bid);
    dump($this->optimized_bid);
    dump($this->min_bid->isGreaterThanOrEqualTo($this->optimized_bid));
    

    This is the output:

    Brick\Money\Money {#3601
      -amount: Brick\Math\BigDecimal {#3612
        -value: "6"
        -scale: 2
      }
      -currency: Brick\Money\Currency {#3594
        -currencyCode: "USD"
        -numericCode: 840
        -name: "US Dollar"
        -defaultFractionDigits: 2
      }
      -context: Brick\Money\Context\DefaultContext {#3608}
    }
    6
    false
    

    I'm getting the same result when comparing with isEqual. This might be me misunderstanding how this is supposed to work, but to me the result is unexpected.

    is 6 equal to 6? yes, or not? lol

    opened by madsem 6
  • what is the default value of null price?

    what is the default value of null price?

    I'm using the "discount price" column in my e-commerce app. When there is no discount for the product the discount price is null, I want to check if this column is null or not in laravel view.

    It seems this package overrides the null value with another, who can I check if there is a discount price or not?

    opened by Huthaifah-kholi 1
  • Update an example of $converter->convert() in readme

    Update an example of $converter->convert() in readme

    Since version 0.6.0 (https://github.com/brick/money/commit/c82e10618719b9ef4f57a9d3531a7a433d901138) the convert() method signature has changed. However, readme was not updated and throws an error:

    ... Argument # 3 ($context) must be of type ?Brick\Money\Context ...

    Since PHP 8.0 (named function arguments) we could use $converter->convert($money, 'EUR', roundingMode: RoundingMode::DOWN);, but the package still supports PHP 7.4, so let's use the null in the example.

    opened by ekateiva 0
  • allocateWithRemainder() performs no allocation

    allocateWithRemainder() performs no allocation

    Prior to this change, Money::of('1', 'USD')->allocateWithRemainder(400, 0, 40, 20, 2) returned 86, 0, 8, 4, 0, 2 (as cents wrapped in Money instances of course), and I distributed the 2 cents remainder by adding one to the 86 figure, one to the 8 figure.

    After the change, Money::of('1', 'USD')->allocateWithRemainder(400, 0, 40, 20, 2) returns 0, 0, 0, 0, 0, 100.

    I understand the reason behind this, however allocateWithRemainder() is no longer helpful in cases like the above, as it does no allocation at all.

    Is there anything within the library that can help me with that? Or should I resort to allocating the 100 cents myself (probably by weight)?

    Thanks!

    opened by ep-01 3
  • Add a compareTo method

    Add a compareTo method

    Add a new method compareTo, in order to get if a value is equals to (0), greater than (1), or less than (-1) the given value.

    $money = Money::of(50, 'USD');
    $money1 = Money::of(60, 'USD');
    $money2 = Money::of(60, 'USD');
    
    echo $money->compareTo($money1); // -1 (less than)
    echo $money1->compareTo($money); // 1 (greter than)
    echo $money->compareTo($money2); // 0 (equals)
    echo $money->compareTo($money); // 0 (equals)
    

    If you try to compare two different currencies an error should be shown

    opened by matutetandil 1
  • No support for adding CurrencyProviders

    No support for adding CurrencyProviders

    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.

    opened by devsi 2
Releases(0.7.0)
  • 0.7.0(Oct 5, 2022)

    💥 Breaking changes

    • JSON extension is now required for PHP 7.4 (always available with PHP >= 8.0)
    • AbstractMoney is now officially sealed, extending it yourself is not supported

    New features

    • Money and RationalMoney now implement JsonSerializable
    Source code(tar.gz)
    Source code(zip)
  • 0.6.0(Aug 1, 2022)

    💥 Breaking changes

    • Minimum PHP version is now 7.4
    • AbstractMoney::getAmount() now has a return type
    • CurrencyConverter's constructor does not accept a default $context anymore
    • CurrencyConverter::convert() now requires the $context previously accepted by the constructor as third parameter
    • Money::allocateWithRemainder() now refuses to allocate a portion of the amount that cannot be spread over all ratios, and instead adds that amount to the remainder (#55)
    • Money::splitWithRemainder() now behaves like allocateWithRemainder()

    New ISO currencies

    • SLE (Leone) in Sierra Leone (SL)

    👌 Improvements

    • Compatibility with brick/math version 0.10
    Source code(tar.gz)
    Source code(zip)
  • 0.5.3(Oct 10, 2021)

  • 0.5.2(Apr 3, 2021)

    New methods

    • Money::splitWithRemainder()
    • Money::allocateWithRemainder()

    These methods perform like their split() and allocate() counterparts, but append the remainder at the end of the returned array instead of spreading it over the first monies.

    Thanks @NCatalani!

    Source code(tar.gz)
    Source code(zip)
  • 0.5.1(Feb 10, 2021)

    👌 Improvement

    BaseCurrencyProvider now always returns a BigNumber for convenience (#37). This is useful if you're using BaseCurrencyProvider on its own, not just in CurrencyConverter.

    Thanks @rdarcy1!

    Source code(tar.gz)
    Source code(zip)
  • 0.5.0(Aug 19, 2020)

    👌 Improvements

    • compatibility with brick/math version 0.9

    ⚠️ Caution

    When using brick/math version 0.9, the Money factory methods such as of() and ofMinor() now accept decimal numbers in the form .123 and 123., and do not throw an exception anymore in this case.

    Source code(tar.gz)
    Source code(zip)
  • 0.4.5(May 31, 2020)

  • 0.4.4(Jan 23, 2020)

    New method

    AbstractMoney::isAmountAndCurrencyEqualTo() compares a money to another. (#17)

    This method is different from isEqualTo() in 2 aspects:

    • it only accepts another money, not a raw number;
    • it returns false if the money is in another currency, instead of throwing an exception.
    Source code(tar.gz)
    Source code(zip)
  • 0.4.3(Jan 9, 2020)

    🛠 Improvements

    • MoneyBag::getAmount() now accepts an ISO numeric currency code as well

    New methods

    • CurrencyConverter::convertToRational() converts to a RationalMoney (#22)
    Source code(tar.gz)
    Source code(zip)
  • 0.4.2(Jul 4, 2019)

  • 0.4.1(Oct 17, 2018)

  • 0.4.0(Oct 9, 2018)

    Breaking Changes

    • Deprecated method BigRational::toMoney() has been removed, use BigRational::to() instead;
    • BigRational::__toString() now always outputs the amount in non-simplified rational form.

    New methods

    • BigRational::simplified() returns a copy of the money with the amount simplified.
    Source code(tar.gz)
    Source code(zip)
  • 0.3.4(Sep 12, 2018)

  • 0.3.3(Aug 22, 2018)

  • 0.3.2(Aug 20, 2018)

    Money::formatTo() can now format the amount as a whole number:

    formatTo(string $locale, bool $allowWholeNumber = false) : string
    

    By default, formatTo() always outputs all the fraction digits:

    Money::of('23.5', 'USD')->formatTo('en_US'); // $23.50
    Money::of(23, 'USD')->formatTo('en_US'); // $23.00
    

    But can now be allowed to return the whole number by passing true as a second argument:

    Money::of('23.5', 'USD')->formatTo('en_US', true); // $23.50
    Money::of(23, 'USD')->formatTo('en_US', true); // $23
    

    Note that this version now requires brick/math version 0.7.3. This is not a BC break. If you've locked your composer.json to an earlier version, you will just not be able to install brick/money version 0.3.2.

    Source code(tar.gz)
    Source code(zip)
  • 0.3.1(Aug 4, 2018)

  • 0.3.0(Jul 26, 2018)

    New methods:

    • CurrencyConversionException::getSourceCurrencyCode()
    • CurrencyConversionException::getTargetCurrencyCode()

    This allows to programmatically get the failing currency pair when an exchange rate is not available.

    Breaking change:

    • CurrencyConversionException constructor signature changed

    Although this is technically a breaking change and requires a version bump, your code is unlikely to be affected, unless you're creating CurrencyConversionException instances manually (you shouldn't).

    Source code(tar.gz)
    Source code(zip)
  • 0.2.4(Jan 10, 2018)

  • 0.1.1(Dec 8, 2017)

    Backports from 0.2.x:

    • New method: CustomContext::getScale()
    • Money::formatTo() now always respects the scale of the Money
    • Bug fix: Money::allocate() incorrectly allocated negative monies
    Source code(tar.gz)
    Source code(zip)
  • 0.2.3(Dec 1, 2017)

  • 0.2.2(Nov 20, 2017)

  • 0.2.1(Nov 5, 2017)

  • 0.2.0(Oct 2, 2017)

    • Minimum requirement is now PHP 7.1
    • BigRational::toMoney() has been deprecated; use to() instead. This is the result of a factorization of a common feature in Money and RationalMoney.
    Source code(tar.gz)
    Source code(zip)
  • 0.1.0(Oct 2, 2017)

Owner
Brick
Building blocks for professional PHP applications
Brick
CPAY is a sdk that encapsulates the Orange Money api with an intuitive syntax allowing you to integrate the Orange Money payment into your PHP project.

CPAY CHOCO PAY is a sdk that encapsulates the Orange Money api with an intuitive syntax allowing you to integrate the Orange Money payment into your P

faso-dev 1 Oct 26, 2022
World Currency list in PHP constants and in array (Currency::USD)

World Currency list in PHP constants and in array (Currency::USD) If you need to work with currencies in the code and describe each time "USD", "EUR"

Krepysh 4 Jun 27, 2022
Library download currency rate and save in database, It's designed to be extended by any available data source.

Library download currency rate and save in database, It's designed to be extended by any available data source.

Flexmind. Krzysztof Bielecki 2 Oct 6, 2021
Laravel package to convert English numbers to Bangla number or Bangla text, Bangla month name and Bangla Money Format

Number to Bangla Number, Word or Month Name in Laravel | Get Wordpress Plugin Laravel package to convert English numbers to Bangla number or Bangla te

Md. Rakibul Islam 50 Dec 26, 2022
> Create e-wallet, send money, withdraw and check balance all via USSD protocol

Mobile Money USSD solution Create e-wallet, send money, withdraw and check balance all via USSD protocol Create e-wallet Step 1 Step 2 Step 3 Step 4 S

Emmanuel HAKORIMANA 1 Nov 3, 2021
Buy and sell crypto top 100 crypto with our fake currency. Donate to and use our referal links for discounts

PLEASE ENABLE SQLITE3 AND GD OR GD2 IN XAMPP TO RUN THE APP! (SEE HOW_TO_SETUP_XAMPP.gif) ![alt text](https://github.com/Tby23rd/Project1-Cryptosimul

Tabitha Maru 0 Dec 26, 2021
A pool for players to mine money

MoneyPool v1.0.2 A pool for players to mine money

Toby 4 Jun 22, 2021
A plugin for working with popular money libraries in Pest

This package is a plugin for Pest PHP. It allows you to write tests against monetary values provided by either brick/money or moneyphp/money using the same declarative syntax you're used to with Pest's expectation syntax.

Luke Downing 19 Oct 30, 2022
The stock market inflation adjusted for the US-money supply

Inflation Chart Inflation Chart is a project to find the intrinsic value of stock markets, stock prices, goods and services by adjusting them to the a

levelsio 96 Apr 14, 2022
A simple package for working with money.

Money A simple package for working with money. Main features: Simple API Livewire integration Custom currency support Highly customizable formatting R

ARCHTECH 143 Nov 18, 2022
Standardized wrapper for popular currency rate APIs. Currently supports FixerIO, CurrencyLayer, Open Exchange Rates and Exchange Rates API.

?? Wrapper for popular Currency Exchange Rate APIs A PHP API Wrapper to offer a unified programming interface for popular Currency Rate APIs. Dont wor

Alexander Graf 24 Nov 21, 2022
Extract and evolution of the magento2-currency-precision module from the magento2-jp project from @Magento

Currency Precision Module for Magento 2 This module aims to help merchants to manage easily their currency precision in Magento 2. DISCLAIMER Initiall

OpenGento 3 Dec 17, 2021
Xero - a digital currency that allows instant payments to anyone, anywhere

Xeros is a digital currency that allows instant payments to anyone, anywhere. Xeros has been written completely in PHP and mostly follows the technical design of Bitcoin. Xeros uses P2P technology to operate with no central server.

Kladskull 79 Dec 26, 2022
Simple Symfony currency exchange demo application (CLI)

Symfony currency exchange demo Keynotes Using a small Symfony installation as possible Using SQLite database for simplicity but with price of some cav

Vladimir Martsul 9 Oct 21, 2022
Dobren Dragojević 6 Jun 11, 2023
MOP is a php query handling and manipulation library providing easy and reliable way to manipulate query and get result in a fastest way

Mysql Optimizer mysql optimizer also known as MOP is a php query handling and manipulation library providing easy and reliable way to manipulate query

null 2 Nov 20, 2021
:date: The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects

sabre/vobject The VObject library allows you to easily parse and manipulate iCalendar and vCard objects using PHP. The goal of the VObject library is

sabre.io 532 Dec 25, 2022
JSONFinder - a library that can find json values in a mixed text or html documents, can filter and search the json tree, and converts php objects to json without 'ext-json' extension.

JSONFinder - a library that can find json values in a mixed text or html documents, can filter and search the json tree, and converts php objects to json without 'ext-json' extension.

Eboubaker Eboubaker 2 Jul 31, 2022
Backwards compatibility Extension and Library for PHP 8.x and later

colopl_bc Provides various compatibility functions required for PHP (temporary) migration. WARNING This extension is intended for temporary use only.

COLOPL,Inc. 10 Jun 13, 2023