The technique for handling currency is similar to that for numbers. We will even use the same NumberFormatter class! There is one major difference, however, and it is a show stopper: in order to properly format currency, you will need to have on hand the currency code.
Application\I18n\Locale class constructor argument:const FALLBACK_CURRENCY = 'GBP';
protected $currencyCode;
public function __construct($localeString = NULL, $currencyCode = NULL)
{
// add this to the existing code:
$this->currencyCode = $currencyCode ?? self::FALLBACK_CURRENCY;
}This approach, although obviously solid and workable, tends to fall into the category called halfway measures or the easy way out! This approach would also tend to eliminate full automation as the currency code is not available from the HTTP header. As you have probably gathered from other recipes in this book, we do not shy away from a more complex solution so, as the saying goes, strap on your seat belts!
Application\I18n\IsoCodes. As you can see, this class has all the pertinent properties, along with a sort-of universal constructor:namespace Application\I18n;
class IsoCodes
{
public $name;
public $iso2;
public $iso3;
public $iso_numeric;
public $iso_3166;
public $currency_name;
public $currency_code;
public $currency_number;
public function __construct(array $data)
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
$this->$key = $data[$key] ?? NULL;
}
}
}Application\I18n\IsoCodesInterface:namespace Application\I18n;
interface IsoCodesInterface
{
public function getCurrencyCodeFromIso2CountryCode($iso2) : IsoCodes;
}Application\I18n\IsoCodesDb. It implements the abovementioned interface, and accepts an Application\Database\Connection instance (see Chapter 1, Building a Foundation), which is used to perform the lookup. The constructor sets up the required information, including the connection, the lookup table name, and the column that represents the ISO2 code. The lookup method required by the interface then issues an SQL statement and returns an array, which is then used to build an IsoCodes instance:namespace Application\I18n;
use PDO;
use Application\Database\Connection;
class IsoCodesDb implements IsoCodesInterface
{
protected $isoTableName;
protected $iso2FieldName;
protected $connection;
public function __construct(Connection $connection, $isoTableName, $iso2FieldName)
{
$this->connection = $connection;
$this->isoTableName = $isoTableName;
$this->iso2FieldName = $iso2FieldName;
}
public function getCurrencyCodeFromIso2CountryCode($iso2) : IsoCodes
{
$sql = sprintf('SELECT * FROM %s WHERE %s = ?', $this->isoTableName, $this->iso2FieldName);
$stmt = $this->connection->pdo->prepare($sql);
$stmt->execute([$iso2]);
return new IsoCodes($stmt->fetch(PDO::FETCH_ASSOC);
}
}Application\I18n\Locale class. We first add a couple of new properties and class constants:const ERROR_UNABLE_TO_PARSE = 'ERROR: Unable to parse'; const FALLBACK_CURRENCY = 'GBP'; protected $currencyFormatter; protected $currencyLookup; protected $currencyCode;
getRegion() method, which comes from the PHP Locale class (which we extend). Just in case it's needed, we also add a method, getCurrencyCode():public function getCountryCode()
{
return $this->getRegion($this->getLocaleCode());
}
public function getCurrencyCode()
{
return $this->currencyCode;
}getCurrencyFormatter(I), much as we did getNumberFormatter() (shown previously). Notice that $currencyFormatter is defined using NumberFormatter, but with a different second parameter:public function getCurrencyFormatter()
{
if (!$this->currencyFormatter) {
$this->currencyFormatter = new NumberFormatter($this->getLocaleCode(), NumberFormatter::CURRENCY);
}
return $this->currencyFormatter;
}public function __construct($localeString = NULL, IsoCodesInterface $currencyLookup = NULL)
{
// add this to the existing code:
$this->currencyLookup = $currencyLookup;
if ($this->currencyLookup) {
$this->currencyCode = $this->currencyLookup->getCurrencyCodeFromIso2CountryCode($this->getCountryCode())->currency_code;
} else {
$this->currencyCode = self::FALLBACK_CURRENCY;
}
}FALSE if the parsing operation is not successful:public function formatCurrency($currency)
{
return $this->getCurrencyFormatter()->formatCurrency($currency, $this->currencyCode);
}
public function parseCurrency($string)
{
$result = $this->getCurrencyFormatter()->parseCurrency($string, $this->currencyCode);
return ($result) ? $result : self::ERROR_UNABLE_TO_PARSE;
}Create the following classes, as covered in the first several bullet points:
|
Class |
Bullet point discussed |
|---|---|
|
|
3 |
|
|
4 |
|
|
5 |
We will assume, for the purposes of this illustration, that we have a populated MySQL database table, iso_country_codes, which has this structure:
CREATE TABLE `iso_country_codes` ( `name` varchar(128) NOT NULL, `iso2` varchar(2) NOT NULL, `iso3` varchar(3) NOT NULL, `iso_numeric` int(11) NOT NULL AUTO_INCREMENT, `iso_3166` varchar(32) NOT NULL, `currency_name` varchar(32) DEFAULT NULL, `currency_code` char(3) DEFAULT NULL, `currency_number` int(4) DEFAULT NULL, PRIMARY KEY (`iso_numeric`) ) ENGINE=InnoDB AUTO_INCREMENT=895 DEFAULT CHARSET=utf8;
Make the additions to the Application\I18n\Locale class, as discussed in bullet points 6 to 9 previously. You can then create a chap_08_formatting_currency.php file, which sets up autoloading and uses the appropriate classes:
<?php
define('DB_CONFIG_FILE', __DIR__ . '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\I18n\Locale;
use Application\I18n\IsoCodesDb;
use Application\Database\Connection;
use Application\I18n\Locale;Next, we create instances of the Connection and IsoCodesDb classes:
$connection = new Connection(include DB_CONFIG_FILE); $isoLookup = new IsoCodesDb($connection, 'iso_country_codes', 'iso2');
For this illustration, create two Locale instances, one for the UK, the other for France. You can also designate a large number to be used for testing:
$localeFr = new Locale('fr-FR', $isoLookup);
$localeUk = new Locale('en_GB', $isoLookup);
$number = 1234567.89;
?>Finally, you can wrap the formatCurrency() and parseCurrency() methods in the appropriate HTML display logic and view the results. Base your view logic on that presented in the How it works... section of the previous recipe (not repeated here to save trees!). Here is the final output:
