How to work with Money in PHP

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 VI 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 denomination 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 mathematics 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 potential 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:

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:

At some point you may want to store a monetary value in the database using an ORM, at VI we use Doctrine’s ORM for persistence 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 persistence.

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 reusable and object-oriented way while also making the project convenient 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.

Article written by:

Kane

As a software engineer at VI, his role at the company is to learn and create quality, well-tested software.

You might also like...