How to work with Money in PHP

Author: Kane
Friday, October 1 2021

TL;DR: Store monetary values in value objects for developer convience whilst also helping you develop reusable code that feels natural to OOP and addresses precision issues.

Floating point numbers are problematic as they lack defined precision, as certain most decimal numbers cannot be accurately stored in a binary system due to the way floating point values are implemented using exponents.

This lack of defined precision can lead to difficult bugs. At Viva IT we have worked with numbers since we were founded and have experienced some of these kind of issues.

Traditionally people have stored and calculated monetary as integers as they don't suffer from the same flaws as floats. As the currency is stored as the fraction denomation of a currency (e.g pence or cent) so £3.14 would be stored as an integer of value 314. In PHP the BC math library is also a viable option, it allows you to perform mathmatic operations on numeric formatted strings. Those these two methods can address precision issues, however they can often lead to obscure and reused code and therefore the potenial for bugs is introduced.

We have recently moved to the library brick/money. This provides a value object for dealing with money values while also calculating and storing other relevant metadata such as currency.

Some advantages of using value objects (not just brick/money) for money are:

  • It allows you to encapsulate metadata about the money value e.g currency, scale, etc.
  • They improve the readability of your code and provide internalisation by default.
  • They can be used as arguments to your functions to ensure that valid monetary values are passed.
  • They allow you to use promote code reuse in a way that feels natural to OOP.

If you can't or don't want to couple parts or any of your system to the brick/money library, then there's no reason you couldn't develop your own value object:

<?php  

declare(strict_types=1);  

namespace YourApp;  

use InvalidArgumentException;  

use function bcadd;  
use function bcmul;  
use function is_numeric;  

final class Money  
{  
     private string $value;  

     public function __construct(int|float|string $value, private string $currency)
     { 
        if (! is_numeric($value)) {  
             throw new InvalidArgumentException('Invalid value');  
         }  

         // Perform rounding appropriate to your currency here
         $this->value = (string)$value;  
     }  

    public function add(Money $other): self  
    {  
         if ($this->currency !== $other->currrency) {  
             throw new InvalidArgumentException('Mismatched currency');  
         }  

         return new self(bcadd($other->value, $this->value, 4), $this->currency);  
     }

     public function divide(int $divisor): self  
     {  
         return new self(bcdiv($this->value, $divisor, 10), $this->currency);
     }  

     //... Add other methods here for other operations you might need such as  
     // multiplication, subtraction, or the ability to convert it into a value for the view. 
}

Important notes about this implementation are:

  • Rounding has not been implemented in this basic example, you can find some examples of how to do this in a BC Math environment here: at Stack Overflow. Note: Brick will perform this for you automatically.
  • This object is immutable (notice that it returns a new object each time). Immutable operations are recommended for value objects as it means that the objects act like a scalar value, this feels more natural for developers because of the existing mental model of they have of money values. Not implementing this value object as immutable could lead to unexpected changes to values in parts of the system.
  • When dividing values in the example above, I specify a higher precisions as certain divisors could lead to remainders. As BC Math doesn't round values it just 'chops down' numbers that are too long, it's important to perform rounding that is compatible with the domain of your system!

At some point you may want to store a monetary value in the database using an ORM, at Viva IT we use Doctrine's ORM for persistance and we brick/money value objects in our entities. This approach will lead to coupling your entities to a third-party value object, of course if this is not an option for your software project feel free to use a custom value object (such as the one above) or if not that is not an option you may want to convert your value objects into string scalar values before persistance.

You can also use Doctrine embeddables, although we opted against this as we would like to natively work with scalars in the database for ETL operations.

Here is a typical example of how we might store Brick Money value objects in a Doctrine entity:

<?php  

declare(strict_types=1);  

namespace YourApp;  

use InvalidArgumentException;
use Brick\Money\Money;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 */
class Account  
{  
    /**  
      * @ORM\Column(type="decimal", precision=15, scale=4)  
     */
     private string $balance;  

    /**  
      * @ORM\Column(type="string", length=3)  
      */
    private string $currency;

     public function __construct(Money $balance)
     {   
        $this->balance = (string)$balance->getAmount();  
        $this->currency = $balance->getCurrency()->getCurrencyCode();
     }  

    public function setBalance(Money $balance): void 
    {
        $this->balance = (string)$balance->getAmount();  
        $this->currency = $balance->getCurrency()->getCurrencyCode();
    }

    public function getBalance(): Money 
    {
        return Money::of($this->balance, $this->currency);
    }
}

In conclusion, a correctly implemented value object (immutable and with the correct rounding for your system) is a great way to deal with monetary values in a reuseable and object-oriented way while also making the project convient to work with for developers. brick/money is a good option for teams who don't mind coupling their coupling their systems to this third-party library. Although if this coupling is a problem, we would advise teams to implement their own value object.

Docker for Mac Performance using NFS (Updated for macOS Catalina) Solar flares can break websites!