cult3

Creating a PHP Shopping Basket Part 7 - Transforming and Formatting

Mar 04, 2015

Table of contents:

  1. Where does the responsibility lie?
  2. The Formatter classes
  3. Converting using the Formatter objects
  4. Transforming an Order
  5. Conclusion

Over the last couple of weeks we’ve looked at creating a PHP shopping basket package.

An important component of an ecommerce application is displaying the order details as part of the user interface. This might be as part of an order confirmation or perhaps an invoice.

It’s therefore inevitable that we will want to use the data from the order as part of an HTML view or perhaps as an JSON HTTP response.

However, the order is comprised of raw values. We need a way to format these raw values for human consumption.

This raises an important point, where does the responsibility lie for formatting the values of an order?

In today’s final instalment of this mini-series we will be looking at transforming and formatting a processed order for human consumption.

Where does the responsibility lie?

One of the big things I want you to take away from this mini-series is how you should analyse and divide responsibility appropriately between objects that comprise a certain bit of functionality.

As I mentioned a couple of times, it’s important to match the responsibilities of the real world to the objects that you create.

A common problem I see with a lot of open source packages is how formatting is dealt with.

The majority of the time when you see an object that needs to be formatted as a human readable value, it will usually just have a method for doing so. This often involves some kind of weird string manipulation.

For example, if you wanted to format a money value:

$price = new Money(10, new Currency("USD"));

$price->format(); // $10.00

I strongly believe that it is not the responsibility of the Money object to provide the functionality to format itself for a couple of reasons.

Firstly, there are many different ways to format money values depending on where in the world you live, and so the Money object would need to take on a lot of extra responsibility for formatting in every possible way.

Secondly, if a developer wanted to format the value in a bespoke way you are forcing them to hack around your internal logic or the API of the object to do so.

And finally, formatting a value is a very different concern to the single responsibility of the Money object’s primary role of representing a monetary value. We should always try to restrict the scope of an object to stay close to the Single Responsibility Principle.

So instead of lumping the logic onto an existing object, a much better solution is to provide a generic and extendable way to format any type of value.

The Formatter classes

So we need to create a generic way of accepting an input and then returning a formatted output.

This means we can define some basic formatting classes to act as defaults. But then a developer can use their own implementation if they require a different style of formatting.

As we’ve seen a couple of times in this mini-series, the first step towards providing this kind of extensibility is by writing an interface:

<?php namespace PhilipBrown\Basket;

interface Formatter
{
    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value);
}

As you can see, this is a really simple interface. However by implementing the interface we can ensure we can accept any concrete implementation.

If you are unfamiliar with PHP interfaces and when to use them, take a look at When should I code to an Interface?.

With the interface in place, we can now start to look at writing the default implementations.

Category Formatter

The first formatter we will write will be for displaying the category of the product. If you remember back to Creating a PHP Shopping Basket Part 3 - Creating the Product object, a category class will be in the format of PhysicalProduct.

I think an appropriate way of formatting this value would be simply to split the class name into two words. Here is the CategoryFormatter implementation:

<?php namespace PhilipBrown\Basket\Formatters;

use PhilipBrown\Basket\Formatter;

class CategoryFormatter implements Formatter
{
    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value)
    {
        $namespace = explode("\\", get_class($value));
        $class = array_pop($namespace);
        $regex =
            "/(?<!^)((?<![[:upper:]])[[:upper:]]|[[:upper:]](?![[:upper:]]))/";

        return preg_replace($regex, ' $1', $class);
    }
}

We can test this implementation with the following test:

<?php namespace PhilipBrown\Basket\Tests\Formatters;

use Money\Money;
use Money\Currency;
use PhilipBrown\Basket\Product;
use PhilipBrown\Basket\Categories\PhysicalBook;
use PhilipBrown\Basket\Formatters\CategoryFormatter;
use PhilipBrown\Basket\TaxRates\UnitedKingdomValueAddedTax;

class CategoryFormatterTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_format_category()
    {
        $sku = "0";
        $name = "Back to the Future Blu-ray";
        $rate = new UnitedKingdomValueAddedTax();
        $price = new Money(1000, new Currency("GBP"));
        $product = new Product($sku, $name, $price, $rate);

        $formatter = new CategoryFormatter();

        $this->assertEquals(
            "Physical Book",
            $formatter->format(new PhysicalBook($product))
        );
    }
}

In this test I’m passing an instance of PhysicalBook and then asserting that the returned value is formatted correctly.

Collection Formatter

When we have a collection of tags or coupons we will need to transform it from an object into a simply array.

To do this we can simply call the toArray() method on the Collection object.

Here is the CollectionFormatter implementation:

<?php namespace PhilipBrown\Basket\Formatters;

use PhilipBrown\Basket\Formatter;

class CollectionFormatter implements Formatter
{
    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value)
    {
        return $value->toArray();
    }
}

Here is the test file:

<?php namespace PhilipBrown\Basket\Tests\Formatters;

use PhilipBrown\Basket\Collection;
use PhilipBrown\Basket\Formatters\CollectionFormatter;

class CollectionFormatterTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_format_collection()
    {
        $formatter = new CollectionFormatter();

        $this->assertEquals(
            [1, 2, 3],
            $formatter->format(new Collection([1, 2, 3]))
        );
    }
}

Percent Formatter

Formatting percentages is fairly simple because we only need to take the value of the object as an int and add a % after it:

<?php namespace PhilipBrown\Basket\Formatters;

use PhilipBrown\Basket\Formatter;
use PhilipBrown\Basket\Percentage;

class PercentFormatter implements Formatter
{
    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value)
    {
        if ($value instanceof Percentage) {
            $value = $value->toPercent();
        }

        return $value->int() . "%";
    }
}

You will notice that I’m checking for an instance of Percentage. This is because we need to deal with percentage discounts and so we need to resolve the discount rate into a Percent object before we format it.

The test class for this file looks like this:

<?php namespace PhilipBrown\Basket\Tests\Formatters;

use PhilipBrown\Basket\Percent;
use PhilipBrown\Basket\Formatters\PercentFormatter;

class PercentFormatterTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_return_formatted_percent()
    {
        $formatter = new PercentFormatter();

        $this->assertEquals("20%", $formatter->format(new Percent(20)));
    }
}

TaxRate Formatter

Formatting a tax rate is fairly similar to formatting a percentage as we simply need to append the % symbol:

<?php namespace PhilipBrown\Basket\Formatters;

use PhilipBrown\Basket\Formatter;

class TaxRateFormatter implements Formatter
{
    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value)
    {
        return $value->percentage() . "%";
    }
}

Here is the test file for this class:

<?php namespace PhilipBrown\Basket\Tests\Formatters;

use PhilipBrown\Basket\Percent;
use PhilipBrown\Basket\Formatters\TaxRateFormatter;
use PhilipBrown\Basket\TaxRates\UnitedKingdomValueAddedTax;

class TaxRateFormatterTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_return_formatted_tax_rate()
    {
        $formatter = new TaxRateFormatter();

        $this->assertEquals(
            "20%",
            $formatter->format(new UnitedKingdomValueAddedTax())
        );
    }
}

Money Formatter

Finally we have the tricky problem of formatting money values.

To make our final objective clearer, it will be easier to write the tests first:

<?php namespace PhilipBrown\Basket\Tests\Formatters;

use Money\Money;
use Money\Currency;
use PhilipBrown\Basket\Formatters\MoneyFormatter;

class MoneyFormatterTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function should_format_as_english_pounds()
    {
        $formatter = new MoneyFormatter("en_GB");

        $this->assertEquals(
            "£10.00",
            $formatter->format(new Money(1000, new Currency("GBP")))
        );
    }

    /** @test */
    public function should_format_as_american_dollars()
    {
        $formatter = new MoneyFormatter("en_US");

        $this->assertEquals(
            '$10.00',
            $formatter->format(new Money(1000, new Currency("USD")))
        );
    }

    /** @test */
    public function should_format_as_european_euros()
    {
        $formatter = new MoneyFormatter("de_DE");

        $this->assertEquals(
            "10,00 €",
            $formatter->format(new Money(1000, new Currency("EUR")))
        );
    }
}

So as you can see, we need to be able to:

  1. Take a Money object and format it for the correct currency.
  2. Format it inline to the specific locale of the customer. (Notice how the is at the end of the string in the final test).

The first thing we will do will be to create the empty class file:

<?php namespace PhilipBrown\Basket\Formatters;

use PhilipBrown\Basket\Formatter;

class MoneyFormatter implements Formatter
{
    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value)
    {
    }
}

By default we will assume that the locale should be set to whatever the server is set to. However, we should make it really easy to override this setting by accepting a bespoke locale through the __construct() method:

/**
 * @var string
 */
private $locale;

/**
 * Create a new Money Formatter
 *
 * @param string $locale
 * @return void
 */
public function __construct($locale = null)
{
    $this->locale = $locale;
}

We’re also going to need some meta data about each currency. To do this we can include a JSON file when the object is created:

if (!isset(static::$currencies)) {
    static::$currencies = json_decode(
        file_get_contents(__DIR__ . "/currencies.json")
    );
}

This will load the contents of the currencies.json file, decode it and then set it on the $currencies property.

Writing the logic to understand every currency in the world and how each should be displayed is not something that is really the responsibility of this package.

Fortunately, PHP already provides this functionality as part of the international extension. This is a set of functions and objects that make internationalising stuff really easy.

We can create a new instance of the NumberFormatter class from the international extension inside of the format() method:

$formatter = new NumberFormatter($this->locale(), NumberFormatter::CURRENCY);

We can pass in the locale and we specify that we want to format as currency.

As with the PercentFormatter, next we can check to see if the value is an instance of MoneyInterface:

if ($value instanceof MoneyInterface) {
    $value = $value->toMoney();
}

This is used for the ValueDiscount object so we can automatically format it as a money value without having to explicitly pass in the Money object.

To format the value as a money value we can pass the amount and the currency to the NumberFormatter instance.

To get the currency, we can simply return the currency code, (e.g USD):

/**
 * Get the currency ISO Code
 *
 * @param Money $value
 * @return string
 */
private function code(Money $value)
{
    return $value->getCurrency()->getName();
}

All of our money values are int, (e.g 1000 would be $10.00).

To convert to the correct unit and subunits, we can use the following two methods:

/**
 * Get the subunits to units divisor
 *
 * @param string $code
 * @return int
 */
private function divisor($code)
{
    return static::$currencies->$code->subunit_to_unit;
}

/**
 * Convert subunits to units
 *
 * @param Money $money
 * @param int $divisor
 * @return float
 */
private function convert(Money $money, $divisor)
{
    return number_format(($money->getAmount() / $divisor), 2, '.', "");
}

The first method gets the divisor from the currencies.json file from earlier.

The second method will format the value.

Finally we can pass the $amount and the $currency to the NumberFormatter object and return the result.

$formatter->formatCurrency($amount, $code);

Here is what the full class looks like:

<?php namespace PhilipBrown\Basket\Formatters;

use Locale;
use Money\Money;
use NumberFormatter;
use PhilipBrown\Basket\Formatter;
use PhilipBrown\Basket\Money as MoneyInterface;

class MoneyFormatter implements Formatter
{
    /**
     * @var string
     */
    private $locale;

    /**
     * @var array
     */
    private static $currencies;

    /**
     * Create a new Money Formatter
     *
     * @param string $locale
     * @return void
     */
    public function __construct($locale = null)
    {
        $this->locale = $locale;

        if (!isset(static::$currencies)) {
            static::$currencies = json_decode(
                file_get_contents(__DIR__ . "/currencies.json")
            );
        }
    }

    /**
     * Format an input to an output
     *
     * @param mixed $value
     * @return mixed
     */
    public function format($value)
    {
        $formatter = new NumberFormatter(
            $this->locale(),
            NumberFormatter::CURRENCY
        );

        if ($value instanceof MoneyInterface) {
            $value = $value->toMoney();
        }

        $code = $this->code($value);
        $divisor = $this->divisor($code);
        $amount = $this->convert($value, $divisor);

        return $formatter->formatCurrency($amount, $code);
    }

    /**
     * Get the locale
     *
     * @return string
     */
    private function locale()
    {
        if ($this->locale) {
            return $this->locale;
        }

        return Locale::getDefault();
    }

    /**
     * Get the currency ISO Code
     *
     * @param Money $value
     * @return string
     */
    private function code(Money $value)
    {
        return $value->getCurrency()->getName();
    }

    /**
     * Get the subunits to units divisor
     *
     * @param string $code
     * @return int
     */
    private function divisor($code)
    {
        return static::$currencies->$code->subunit_to_unit;
    }

    /**
     * Convert subunits to units
     *
     * @param Money $money
     * @param int $divisor
     * @return float
     */
    private function convert(Money $money, $divisor)
    {
        return number_format($money->getAmount() / $divisor, 2, ".", ");
    }
}

Converting using the Formatter objects

With the default formatter implementations in place, we now need a generic way of accepting a value and determining which formatter instance to use.

By encapsulating this process we make it a lot easier on the developer as they won’t have to write it themselves.

Hopefully this process should work like “magic”!.

To achieve this we can create a new Converter class to encapsulate this process:

<?php namespace PhilipBrown\Basket;

class Converter
{
}

The first thing I will do will be to bootstrap the class with the default implementations of the formatter classes we created earlier:

/**
 * @var array
 */
private $formatters;

/**
 * Create a new Converter
 *
 * @param array $formatters
 * @return void
 */
public function __construct(array $formatters = [])
{
    $bootstrap = [
        'Collection' => new CollectionFormatter,
        'Percent' => new PercentFormatter,
        'TaxRate' => new TaxRateFormatter,
        'Money' => new MoneyFormatter,
        'ValueDiscount' => new MoneyFormatter,
        'Category' => new CategoryFormatter,
        'PercentageDiscount' => new PercentFormatter
    ];

    $this->formatters = array_merge($bootstrap, $formatters);
}

If the developer would like to override any of these default implementations she can simply pass in an array with the correct key to override.

The main public method of this class will be the convert() method:

/**
 * Convert a value using to the appropriate format
 *
 * @param mixed $value
 * @return mixed
 */
public function convert($value)
{
    if (! is_object($value)) return $value;

    return $this->formatter($value)->format($value);
}

We will only be formatting objects, and so if the $value is not an object, we can simply return it. This means we don’t need to deal with any logic for only passing certain types to the convert() class.

If the $value is an object we can pass it to the formatter() method:

/**
 * Get the Formatter for an object
 *
 * @param mixed $object
 * @return Formatter
 */
public function formatter($object)
{
    $interfaces = class_implements($object);

    foreach ($interfaces as $interface) {
        $class = $this->getClassName($interface);

        if (isset($this->formatters[$class])) {
            return $this->formatters[$class];
        }
    }

    $class = $this->getClassName(get_class($object));

    return $this->formatters[$class];
}

The formatter() method will pull any interfaces that the class implements and then cycle through each one to see if there is a registered Formatter instance. If there is, we can return that instance.

If not, we can get the name of the class using the getClassName() method, and then return the registered formatter instance.

The getClassName() method looks like this:

/**
 * Get the class name from the full namespace
 *
 * @param string $namespace
 * @return string
 */
private function getClassName($namespace)
{
    $namespace = explode('\\', $namespace);

    return array_pop($namespace);
}

This method simply snaps off the actual name of the class from the end of the namespace.

The full Converter class should look like this:

<?php namespace PhilipBrown\Basket;

use PhilipBrown\Basket\Formatters\MoneyFormatter;
use PhilipBrown\Basket\Formatters\PercentFormatter;
use PhilipBrown\Basket\Formatters\TaxRateFormatter;
use PhilipBrown\Basket\Formatters\CategoryFormatter;
use PhilipBrown\Basket\Formatters\CollectionFormatter;

class Converter
{
    /**
     * @var array
     */
    private $formatters;

    /**
     * Create a new Converter
     *
     * @param array $formatters
     * @return void
     */
    public function __construct(array $formatters = [])
    {
        $bootstrap = [
            "Collection" => new CollectionFormatter(),
            "Percent" => new PercentFormatter(),
            "TaxRate" => new TaxRateFormatter(),
            "Money" => new MoneyFormatter(),
            "ValueDiscount" => new MoneyFormatter(),
            "Category" => new CategoryFormatter(),
            "PercentageDiscount" => new PercentFormatter(),
        ];

        $this->formatters = array_merge($bootstrap, $formatters);
    }

    /**
     * Convert a value using to the appropriate format
     *
     * @param mixed $value
     * @return mixed
     */
    public function convert($value)
    {
        if (!is_object($value)) {
            return $value;
        }

        return $this->formatter($value)->format($value);
    }

    /**
     * Get the Formatter for an object
     *
     * @param mixed $object
     * @return Formatter
     */
    public function formatter($object)
    {
        $interfaces = class_implements($object);

        foreach ($interfaces as $interface) {
            $class = $this->getClassName($interface);

            if (isset($this->formatters[$class])) {
                return $this->formatters[$class];
            }
        }

        $class = $this->getClassName(get_class($object));

        return $this->formatters[$class];
    }

    /**
     * Get the class name from the full namespace
     *
     * @param string $namespace
     * @return string
     */
    private function getClassName($namespace)
    {
        $namespace = explode("\\", $namespace);

        return array_pop($namespace);
    }
}

We can test the Converter class by passing values to the convert() method and then asserting that it is formatted correctly:

<?php namespace PhilipBrown\Basket\Tests;

use Money\Money;
use Money\Currency;
use PhilipBrown\Basket\Percent;
use PhilipBrown\Basket\Converter;
use PhilipBrown\Basket\Formatters\MoneyFormatter;
use PhilipBrown\Basket\Formatters\PercentFormatter;

class ConverterTest extends \PHPUnit_Framework_TestCase
{
    /** @var Coverter */
    private $converter;

    public function setUp()
    {
        $this->converter = new Converter();
    }

    /** @test */
    public function should_convert_money()
    {
        $output = $this->converter->convert(
            new Money(1000, new Currency("GBP"))
        );

        $this->assertEquals("£10.00", $output);
    }

    /** @test */
    public function should_convert_percent()
    {
        $output = $this->converter->convert(new Percent(20));

        $this->assertEquals("20%", $output);
    }
}

So as you can see, it doesn’t matter what we pass to the convert() method, we should “magically” be returned the correctly formatted value.

Transforming an Order

Now that we have the formatter implementations and we have an automatic process for converting objects, we can now create a classes to encapsulate this process.

You should be able to transform an order into many different formats depending on what you need to do with it. For example, if you want to include the data in a view you will want it as an array. Or if you want to return it as the body of an HTTP response you will probably want it as JSON or XML.

Once again we can define an interface so that each implementation is interchangeable:

<?php namespace PhilipBrown\Basket;

interface Transformer
{
    /**
     * Transform the Order
     *
     * @param Order $order
     * @return mixed
     */
    public function transform(Order $order);
}

Next I will create the first implementation for converting an Order into an array of values:

<?php namespace PhilipBrown\Basket\Transformers;

class ArrayTransformer implements Transformer
{
}

The first thing I will do will be to inject an instance of Converter from earlier:

/**
 * @var Converter
 */
private $converter;

/**
 * Create a new ArrayTransformer
 *
 * @param Converter $converter
 * @return void
 */
public function __construct(Converter $converter)
{
    $this->converter = $converter;
}

Next we can implement the transform() method:

/**
 * Transform the Order
 *
 * @param Order $order
 * @return mixed
 */
public function transform(Order $order)
{
    foreach ($order->meta() as $key => $total) {
        $payload[$key] = $this->converter->convert($total);
    }

    $payload['products'] = [];

    foreach ($order->products() as $product) {
        $payload['products'][] = array_map(function ($value) {
            return $this->converter->convert($value);
        }, $product);
    }

    return $payload;
}

This method basic just iterates over the meta data and products, runs them through the Converter and then stores the output in an array.

Here is the full class:

<?php namespace PhilipBrown\Basket\Transformers;

use PhilipBrown\Basket\Order;
use PhilipBrown\Basket\Converter;
use PhilipBrown\Basket\Transformer;

class ArrayTransformer implements Transformer
{
    /**
     * @var Converter
     */
    private $converter;

    /**
     * Create a new ArrayTransformer
     *
     * @param Converter $converter
     * @return void
     */
    public function __construct(Converter $converter)
    {
        $this->converter = $converter;
    }

    /**
     * Transform the Order
     *
     * @param Order $order
     * @return mixed
     */
    public function transform(Order $order)
    {
        foreach ($order->meta() as $key => $total) {
            $payload[$key] = $this->converter->convert($total);
        }

        $payload["products"] = [];

        foreach ($order->products() as $product) {
            $payload["products"][] = array_map(function ($value) {
                return $this->converter->convert($value);
            }, $product);
        }

        return $payload;
    }
}

Conclusion

And there you have it, we have finally finished creating the PHP Shopping Basket package!

In today’s tutorial we looked at the responsibility of formatting objects for human consumption. I think it is fair to say that an object should not have the responsibility for formatting, particular when the rules around formatting are complicated, such as the case is with money and currency.

We also looked at how we could write a generic process for automagically converting inputs to outputs, and then how we can write simple implementations for converting into an appropriate formate such as an array or JSON.

I hope you have found value in this walkthrough process of building an Open Source PHP package. Although the implementation of a shopping basket might not have been applicable to your problems, I hope you have learned some new tricks when it comes to writing code, thinking about design and building something that is extensible.

You can find the source code for this package at github.com/philipbrown/basket.

I will also be making it available on Packagist once I tie up some lose ends and ship the 1.0.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.