cult3

Working with Money and Currency in PHP

Jun 04, 2014

Table of contents:

  1. Working with Currency
  2. Working with Money
  3. The importance of equality
  4. Running calculations on Money objects
  5. Working with this package
  6. Conclusion

In last week’s tutorial I talked about the potential pit falls when working with money and currency in your applications. There are many things to be aware of when working with money in an application, and there are a lot of best practices that should be consistently observed throughout your code base.

In this tutorial I will be showing you what goes in to abstracting many of these best practices into a PHP package for working with Money. By abstracting this code into it’s own package we can drop it in to any PHP application that requires working with money. This means we don’t have to think about any of these implementation details in each application.

I’m going to assume that you’ve already set up the package structure that adheres to the PSR-4 standard that I outlined in this tutorial.

Working with Currency

As I mentioned last week, managing different currencies will be an extremely important aspect of building an application that can handle international trade.

There are many different types of currency in the world and there is a lot of data that we need for each type of currency. Fortunately the good world of Open Source has already researched and provided a list of meta data that we can use without having to do the work ourselves. This is the beauty of Open Source.

Create a new directory under your src directory called config. Next copy the two json files from the RubyMoney repository on GitHub. The package we will be creating is a shameless PHP port of RubyMoney.

These two json files act like the persistent storage of all of the different types of currencies in the world.

Next we need to create an object for working with these different types of currencies.

Create a new file under the src directory called Currency.php and set class properties for each key of the json config:

<?php namespace PhilipBrown\Money;

class Currency
{
    /**
     * @var int
     */
    protected $priority;

    /**
     * @var string
     */
    protected $iso_code;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $symbol;

    /**
     * @var array
     */
    protected $alternate_symbols;

    /**
     * @var string
     */
    protected $subunit;

    /**
     * @var int
     */
    protected $subunit_to_unit;

    /**
     * @var bool
     */
    protected $symbol_first;

    /**
     * @var string
     */
    protected $html_entity;

    /**
     * @var string
     */
    protected $decimal_mark;

    /**
     * @var string
     */
    protected $thousands_separator;

    /**
     * @var int
     */
    protected $iso_numeric;
}

In order to create a new Currency object, we will require the name of the currency to fetch. Add the following __construct() method to the class.

/**
 * Create a new instance of Currency
 *
 * @param string $name
 * @return void
 */
public function __construct($name)
{
    $name = strtolower($name);

    $currencies = array_merge(
        json_decode(file_get_contents(__DIR__.'/config/currency_iso.json'), true),
        json_decode(file_get_contents(__DIR__.'/config/currency_non_iso.json'), true)
    );

    if (!array_key_exists($name, $currencies)) {
        throw new InvalidCurrencyException("$name is not a valid currency");
    }

    $this->priority = $currencies[$name]['priority'];
    $this->iso_code = $currencies[$name]['iso_code'];
    $this->name = $currencies[$name]['name'];
    $this->symbol = $currencies[$name]['symbol'];
    $this->alternate_symbols = $currencies[$name]['alternate_symbols'];
    $this->subunit = $currencies[$name]['subunit'];
    $this->subunit_to_unit = $currencies[$name]['subunit_to_unit'];
    $this->symbol_first = $currencies[$name]['symbol_first'];
    $this->html_entity = $currencies[$name]['html_entity'];
    $this->decimal_mark = $currencies[$name]['decimal_mark'];
    $this->thousands_separator = $currencies[$name]['thousands_separator'];
    $this->iso_numeric = $currencies[$name]['iso_numeric'];
}

In this method we accept the name of the currency as an argument and then convert it to a lowercase string.

Next we get the contents of the two json files and merge them into an array to work with. If the supplied name of the currency is not one of the currencies in the json meta data we can just bail out here with an exception.

If the currency is found in the json meta data, we can hydrate all of the class properties.

As with the previous package tutorials, create a new directory called Exception and copy the following custom exception class:

<?php namespace PhilipBrown\Money\Exception;

use Exception;

class InvalidCurrencyException extends Exception
{
}

The Currency object is a Value Object that will make working with different currencies in the package a lot easier. To finish off the class, we will need some getter methods that will provide a consistent API for working with each Currency object. I’ve also added a static init method for some syntactical sugar as well as a __toString() method for casting the object to a string.

Here is the full class:

<?php namespace PhilipBrown\Money;

use PhilipBrown\Money\Exception\InvalidCurrencyException;

class Currency
{
    /**
     * @var int
     */
    protected $priority;

    /**
     * @var string
     */
    protected $iso_code;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $symbol;

    /**
     * @var array
     */
    protected $alternate_symbols;

    /**
     * @var string
     */
    protected $subunit;

    /**
     * @var int
     */
    protected $subunit_to_unit;

    /**
     * @var bool
     */
    protected $symbol_first;

    /**
     * @var string
     */
    protected $html_entity;

    /**
     * @var string
     */
    protected $decimal_mark;

    /**
     * @var string
     */
    protected $thousands_separator;

    /**
     * @var int
     */
    protected $iso_numeric;

    /**
     * Create a new instance of Currency
     *
     * @param string $name
     * @return void
     */
    public function __construct($name)
    {
        $name = strtolower($name);

        $currencies = array_merge(
            json_decode(
                file_get_contents(__DIR__ . "/config/currency_iso.json"),
                true
            ),
            json_decode(
                file_get_contents(__DIR__ . "/config/currency_non_iso.json"),
                true
            )
        );

        if (!array_key_exists($name, $currencies)) {
            throw new InvalidCurrencyException("$name is not a valid currency");
        }

        $this->priority = $currencies[$name]["priority"];
        $this->iso_code = $currencies[$name]["iso_code"];
        $this->name = $currencies[$name]["name"];
        $this->symbol = $currencies[$name]["symbol"];
        $this->alternate_symbols = $currencies[$name]["alternate_symbols"];
        $this->subunit = $currencies[$name]["subunit"];
        $this->subunit_to_unit = $currencies[$name]["subunit_to_unit"];
        $this->symbol_first = $currencies[$name]["symbol_first"];
        $this->html_entity = $currencies[$name]["html_entity"];
        $this->decimal_mark = $currencies[$name]["decimal_mark"];
        $this->thousands_separator = $currencies[$name]["thousands_separator"];
        $this->iso_numeric = $currencies[$name]["iso_numeric"];
    }

    /**
     * Create a new Currency object
     *
     * @param string $name
     * @return PhilipBrown\Money\Currency
     */
    public static function init($name)
    {
        return new Currency($name);
    }

    /**
     * Get Priority
     *
     * @return string
     */
    public function getPriority()
    {
        return $this->priority;
    }

    /**
     * Get ISO Code
     *
     * @return string
     */
    public function getIsoCode()
    {
        return $this->iso_code;
    }

    /**
     * Get Name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Get Symbol
     *
     * @return string
     */
    public function getSymbol()
    {
        return $this->symbol;
    }

    /**
     * Get Alternate Symbols
     *
     * @return string
     */
    public function getAlternateSymbols()
    {
        return $this->alternate_symbols;
    }

    /**
     * Get Subunit
     *
     * @return string
     */
    public function getSubunit()
    {
        return $this->subunit;
    }

    /**
     * Get Subunit to unit
     *
     * @return string
     */
    public function getSubunitToUnit()
    {
        return $this->subunit_to_unit;
    }

    /**
     * Get Symbol First
     *
     * @return string
     */
    public function getSymbolFirst()
    {
        return $this->symbol_first;
    }

    /**
     * Get HTML Entity
     *
     * @return string
     */
    public function getHtmlEntity()
    {
        return $this->html_entity;
    }

    /**
     * Get Decimal Mark
     *
     * @return string
     */
    public function getDecimalMark()
    {
        return $this->decimal_mark;
    }

    /**
     * Get Thousands Seperator
     *
     * @return string
     */
    public function getThousandsSeperator()
    {
        return $this->thousands_separator;
    }

    /**
     * Get ISO Numberic
     *
     * @return string
     */
    public function getIsoNumeric()
    {
        return $this->iso_numeric;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->getName();
    }
}

Working with Money

Now that we can represent currencies as an object in the package, we also need to be able to represent money values as objects too.

Create a new class called Money.php under the src directory:

<?php namespace PhilipBrown\Money;

class Money
{
}

Money is a value object in the world of programming and so to create a new Money object we need to supply the value and the type of Currency:

/**
 * The fractional unit of the value
 *
 * @var int
 */
protected $fractional;

/**
 * The currency of the value
 *
 * @var PhilipBrown\Money\Currency
 */
protected $currency;

/**
 * Create a new instance of Money
 *
 * @param int $fractional
 * @param PhilipBrown\Money\Currency $currency
 * @return void
 */
public function __construct($fractional, Currency $currency)
{
    $this->fractional = $fractional;
    $this->currency = $currency;
}

Again as with the Currency object, I will add a little bit of syntactical sugar by providing a static init() method:

/**
 * A static function to create a new instance of Money.
 *
 * @param int $value
 * @param string $currency
 * @return PhilipBrown\Money\Money
 */
public static function init($value, $currency)
{
    return new Money($value, new Currency($currency));
}

And just like the Currency object, I will provide two getter methods for accessing the protected properties of the class:

/**
 * Get the fractional value of the object
 *
 * @return int
 */
public function getCentsParameter()
{
    return $this->fractional;
}

/**
 * Get the Currency object
 *
 * @return PhilipBrown\Money\Currency
 */
public function getCurrencyParameter()
{
    return $this->currency;
}

I will also provide a __get() magic method so the cents and currency class properties will automatically call the two methods above:

/**
 * Magic method to dynamically get object parameters
 *
 * @return mixed
 */
public function __get($param)
{
    $method = 'get'.ucfirst($param).'Parameter';

    if (method_exists($this, $method))

    return $this->{$method}();
}

As I covered in What are PHP Magic Methods, this magic method will be called when you try to access a class property that is not public. In this case the __get() method will accept that property you tried to access as an argument and check to see if the corresponding getter method is defined.

The importance of equality

When working with money in an application, equality is extremely important. For example, it would be very bad if you could add two values of two different currencies together as this would cause an accounting nightmare.

To ensure that two Money objects have the same currency, we can add a method to run a check:

/**
 * Check the Iso code to evaluate the equality of the currency
 *
 * @param PhilipBrown\Money\Money
 * @return bool
 */
public function isSameCurrency(Money $money)
{
    return $this->currency->getIsoCode() == $money->currency->getIsoCode();
}

As I mentioned in What are the differences between Entities and Value Objects, Value Objects base equality on the object’s attributes, rather than their identity.

We can add a method to check to see if an instance of Money is equal to the current instance of Money with this method:

/**
 * Check the equality of two Money objects.
 * First check the currency and then check the value.
 *
 * @param PhilipBrown\Money\Money
 * @return bool
 */
public function equals(Money $money)
{
    return $this->isSameCurrency($money) && $this->cents == $money->cents;
}

It’s important to check that both the currency and the value are the same so we can assert that two Money objects are equal.

Running calculations on Money objects

It’s inevitable that within an ecommerce application you will be required to run calculations on Money objects. Probably the most common instance of this would be adding the value of two products together:

/**
 * Add the value of two Money objects and return a new Money object.
 *
 * @param PhilipBrown\Money\Money $money
 * @return PhilipBrown\Money\Money
 */
public function add(Money $money)
{
    if ($this->isSameCurrency($money)) {
        return Money::init($this->cents + $money->cents, $this->currency->getIsoCode());
    }

    throw new InvalidCurrencyException("You can't add two Money objects with different currencies");
}

As I mentioned above, first we have to check that the two Money objects are the same currency before we add them together.

If the two objects are the same currency we can take the value of the current object and the value of the object we want to add to, as well as the currency to create a new Money object. Remember, Value Objects are immutable and so you if you want to change the value of a Value Object, you have to destroy the current object and create a new one.

Finally, if the two currencies are not the same currency we can throw an exception because this is a really bad thing to happen.

The subtract() method is basically exactly the same but subtracting the values, rather than adding them together:

/**
 * Subtract the value of one Money object from another and return a new Money object
 *
 * @param PhilipBrown\Money\Money $money
 * @return PhilipBrown\Money\Money
 */
public function subtract(Money $money)
{
    if ($this->isSameCurrency($money)) {
        return Money::init($this->cents - $money->cents, $this->currency->getIsoCode());
    }

    throw new InvalidCurrencyException("You can't subtract two Money objects with different currencies");
}

Multiplying or dividing Money objects is slightly more tricky because we are likely to run into situations where we need to round the value to a whole number.

The PHP round() function can solve this problem for us.

To multiply a Money object I will use this method:

/**
 * Multiply two Money objects together and return a new Money object
 *
 * @param int $number
 * @return PhilipBrown\Money\Money
 */
public function multiply($number)
{
    return Money::init((int) round($this->cents * $number, 0, PHP_ROUND_HALF_EVEN), $this->currency->getIsoCode());
}

And for dividing a Money object I will use this method:

/**
 * Divide one Money object and return a new Money object
 *
 * @param int $number
 * @return PhilipBrown\Money\Money
 */
public function divide($number)
{
    return Money::init((int) round($this->cents / $number, 0, PHP_ROUND_HALF_EVEN), $this->currency->getIsoCode());
}

Notice in both of these methods I’m passing in a precision value of 0 and a mode of PHP_ROUND_HALF_EVEN into the round() function. You need to ensure that the way you choose to round numbers is consistent throughout your application.

Working with this package

To create a new Money object you either instantiate like you normally would, or use the init() static convenience method.

// Create a new Money object representing $5 USD
$m = Money::init(500, "USD");
$m = new Money(500, "USD");

To access the value of the Money object you can simply request the cents property. To get the currency of the object you can request the currency property:

$m->cents; // 500
$m->currency; // United States Dollar

Equality is important to working with many different types of currency. You shouldn’t be able to blindly add two different currencies without some kind of exchange process:

$m = Money::init(500, "USD");
$m->isSameCurrency(Money::init(500, "GBP")); // false

A Value Object is an object that represents an entity whose equality isn’t based on identity: i.e. two value objects are equal when they have the same value, not necessarily being the same object:

$one = Money::init(500, "USD");
$two = Money::init(500, "USD");
$three = Money::init(501, "USD");

$one->equals($two); // true
$one->equals($three); // false

Inevitably you are going to need to add, subtract, multiply and divide values of money in your application:

$one = Money::init(500, "USD");
$two = Money::init(500, "USD");

$three = $one->add($two);
$three->cents; // 1000

Again, you shouldn’t be able to add to values of different currencies without some kind of exchange process:

$one = Money::init(500, "USD");
$two = Money::init(500, "GBP");

$three = $one->add($two); // Money\Exception\InvalidCurrencyException

Conclusion

In this tutorial we’ve created a package that abstracts a lot of the headaches away when dealing with money. We’ve created objects that represent money and currency and we’ve implemented some rules around how different values can be calculated, stored and evaluated within your application.

Using this package will prevent a number of problems and pitfalls that you might encounter when working with money and currency in your application. However, as I outlined in How to handle money and currency in web applications, there are still a number of things we need to think about when working with money in software.

If you would like to use this package in one of your projects, it is available on GitHub. Shout out to RubyMoney and Mathias Verraes for the inspiration.

Next week I will be showing you how to build a package that can consume this Money package to abstract more of the headache away from working with money in your applications. By layering these small, concentrated packages, we can create a strong foundation for building high quality ecommerce websites and applications.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.