Currencies are just like timezones, in that they are very annoying to support. They vary by country; many of which share the same symbol, some countries have the symbol before the value and others after — and of course, many European nations use commas to separate decimal places instead of full stops.

Trying to convert from one currency to another might seem easy, but then you’ve got the audacious challenge of trying to do this cleanly knowing fair well customers in Europe and Asia expect prices to be displayed differently (or vice versa, as the case may be).

Alas, I needed to face up to the challenge. I run an online Agile retrospective platform with teams as far away as China, India and the United States, and customers were demanding localised pricing. Despite being a UK business, the platform has a global audience; so a problem I must indeed address.

If you’re in the same boat, here’s how you can integrate currency conversions and localised pricing into your own product.

Tackling currency conversions

There is a great Laravel package from Daniel Stainback available at torann/currency. Fortunately, his package helps us with the boring details — getting the latest exchange rates and converting currencies from one format to another.

The documentation provided misses a number of important details which you’ll find helpful — perhaps even essential — in order to use this package.

Installing the package

$ composer require torann/currency

Configuring the package

You don’t need to register the service provider or aliases. They will be auto-discovered by Laravel.

Publish the configuration files

$ php artisan vendor:publish --provider="Torann\Currency\CurrencyServiceProvider" --tag=config $ php artisan vendor:publish --provider="Torann\Currency\CurrencyServiceProvider" --tag=migrations

Run the migration

$ php artisan migrate

This creates a currencies table that will not just store the exchange rate information but the correct formatting for each currency as well.

Adding the supported currencies

While there is a long list of currencies the package supports, we need to tell it what currencies we support within our application. We therefore need to add the supported currencies into the currencies table.

There is an included helper command to do this:

$ php artisan currency:manage add usd,gbp,cad,aud,sek,inr,cny,eur

You can run currency:manage for each currency or pass a comma-separated list, as shown above.

Updating the exchange rate data

The package includes support for OpenExchangeRate and Google Finance — the latter being an undocumented alternative. What is also undocumented is how you can actually get the package to update the exchange rate data instead of it doing nothing at all.

Add your OpenExchangeRate API key

Before we can update the exchange rate data, the package needs an API key in order to communicate with OpenExchangeRate. Fortunately, they offer a free API key if you need fewer than 1,000 requests a month.

Once you’ve got your API key, add it to your .env file:

OPEN_EXCHANGE_RATE_KEY=<API KEY>

Unfortunately, this environment variable won’t work without changing the config/currency.php file. Why? Here’s why:

/*

|-------------------------------------------------------------------

| API Key for OpenExchangeRates.org

|-------------------------------------------------------------------

|

| Only required if you with to use the Open Exchange Rates api.

| You can always just use Yahoo, the current default.

|

*/ 'api_key' => '',

We need to fix that. Replace the key-value pair with this:

'api_key' => env('OPEN_EXCHANGE_RATE_KEY'),

Note: Yahoo! is no longer a supported exchange provider, despite the inline documentation suggesting otherwise.

Updating the stored exchange rates

The documentation doesn’t state that in order to update the exchange rates, you need to pass a flag to indicate which exchange provider you want to pull the updates from:

$ php artisan currency:update -o

OpenExchangeRates provides free hourly updates (with the base currency set at USD) with a 1,000 request limit per month, which is just enough to send one request per hour for the latest exchange rates.

You might choose to add a cronjob that runs every hour or perhaps once a day to do this. If you use Laravel Forge, you can add a cron task easily from the Scheduler tab of your server — you just need to pass an absolute path to the command you want to run and Forge will do the rest:

php /home/forge/<PROJECT LOCATION>/ artisan currency:update -o .

If you need to set up the cron manually, all I will give you is a link to crontab.guru to verify the cron expression you plan to add and a stark warning to be careful. The crontab will not tell you if a command fails to run.

For hourly runs:

0 * * * *

See: https://crontab.guru/#0_*_*_*_*

For nightly runs:

0 0 * * *

See: https://crontab.guru/#0_0_*_*_*

Formatting prices for a given currency

Let’s open artisan tinker and see how we can take a generic price and properly format it for the given currency.

currency_format(5.99, 'EUR') => "5,99 €"

If you try formatting for a currency that isn’t supported (not listed in the currencies table), it will return an empty string, so you’ll need to use a ternary to return the default currency:

currency_format(5.99, 'JPY') ?: currency_format(5.99)

When a currency code isn’t supplied to the currency_format() function, the default is used. You can change the default in the config/currency.php file:

/*

|-------------------------------------------------------------------

| Application Currency

|-------------------------------------------------------------------

|

| The application currency determines the default currency that will | be used by the currency service provider. You are free to set

| this value to any of the currencies which will be supported

| by the application.

|

*/ 'default' => 'USD',

Again, like the OpenExchangeRates API key, you may want to configure the default currency for your application within your .env file:

'default' => env('DEFAULT_CURRENCY', 'USD'),

On the other hand, some people may prefer to keep it hard-coded so as to require a commit to change it. I’ll leave the choice with you.

Converting prices from one currency to another

The currency_format() function provides a simple wrapper around the PHP NumberFormatter class, but the power of this package really shines through when you need to convert and format prices at once:

currency(5.99, 'USD', 'EUR'); => "5,27 €"

This takes the current exchange rate between the US dollar and the euro and finds how much US$5.99 is worth in euros.

Allowing users to set their own default currency

Your end users can set their own default currency with ease by simply passing the ?currency=<code> GET parameter to any URL within your project. You can then retrieve the users’ selected currency by simply passing it in:

currency(5.99, 'USD', currency()->getUserCurrency()); => "$1.00"

The currency()->getUserCurrency() method returns the user-supplied currency, if one exists and is valid. If the currency provided by the user is invalid or perhaps a currency with which your app does not support, the library disregards it via its middleware (see below) and sets the user currency to the default supplied in config/currency.php.

Adding the middleware to support the GET parameter

The library provides a useful middleware that sets the user-supplied currency into session storage for you. Each time the middleware runs, it checks for the presence of the currency GET parameter; if that’s missing, it will then check for the presence of the currency key in session storage. In all cases, it verifies the provided currency or uses the default before setting it with currency()->setUserCurrency().

Open your app/Kernel.php file and include the CurrencyMiddleware class beneath the StartSession class within the web middleware group:

/**

* The application's route middleware groups.

*

* @var array

*/

protected $middlewareGroups = [

'web' => [

// \Illuminate\Session\Middleware\StartSession::class,

\Torann\Currency\Middleware\CurrencyMiddleware::class,



//

], 'api' => [

'throttle:60,1',

'bindings',

],

];

Remember, the CurrencyMiddleware retrieves and sets sessions, so it must be included beneath the StartSession middleware.

The class instance the currency() function returns is actually a singleton, hence why an unlimited number of calls to get the default currency will always return the currency set by the middleware and not just the default.

Set the supported currencies to active for the middleware

There is one small gotcha you need be aware of. Unfortunately, the Currency class checks to see whether the currency is active before allowing it to be set as the default (i.e. with currency()->setUserCurrency()). This is presumably in the slim chance you don’t want specific currencies being set as the users’ default from the outside world.

For reasons unknown, the author does not provide any means of ‘enabling’ currencies without modifying the database records directly. You’ll need to set the active field to 1 for the currencies that you’re happy for users to be able to set as their default.

To add insult to injury, setting the active field to have a default value of true in the table’s migration is of no use, because currency:mange add explicitly sets the records’ active field to false each time.

Unfortunately, you’ll need to create a database seed or migration to store the supported currencies. I keep mine in database/seeds/raw/currencies.sql. If you have a different idea, please share it with others in the comments section.

Automatically set the users’ currency by location

The final challenge is to try and be helpful to our users by setting a default currency that, 9 times out of 10, will hopefully be their actual local currency. Daniel created another package for this purpose available at torann/geoip.

A crucial feature is that this library returns the currency code corresponding to the country of origin for a given IP address.

Installing the package

$ composer require torann/geoip

Configuring the package

You don’t need to register the service provider or aliases. They will be auto-discovered by Laravel.

Publish the configuration file

$ php artisan vendor:publish --provider="Torann\GeoIP\GeoIPServiceProvider" --tag=config

This will be published to config/geoip.php.

Modify the configuration file

The library uses cache tags but this is not supported on Laravel installations that use the file or database cache driver. In these situations, turn it off:

/*

|------------------------------------------------------------------

| Cache Tags

|------------------------------------------------------------------

|

| Cache tags are not supported when using the file or database cache

| drivers in Laravel. This is done so that only locations can be

| cleared.

|

*/ 'cache_tags' => false,

Using the IP-API service

Of course, we need to use an external provider to identify the country with which a given IP address belongs. A popular (and free) supported provider is IP-API and use of it requires no API key whatsoever.

They allow up to 150 requests per minute before they start automatically banning IP addresses. They have a ‘pro service’ that includes commercial use of their API endpoint with unlimited queries.

The GeoIP package will cache the queried IP locations so only unique requests are sent to whichever provider is used.

IP-API is the default provider so you don’t need to do anything. If you need to purchase their ‘pro service’, set the API key in your .env file as follows:

IPAPI_KEY=<KEY>

Setting the default currency for the user

You can make use of the geoip() function to retrieve the geographic data for a given IP address:

geoip(request()->getClientIp()); // Toraan\GeoIP\Location geoip(request()->getClientIp())->getAttribute('currency'); // "USD"

For consistency’s sake, it’s probably ideal to put the logic for setting the default local currency in a middleware of your own that runs just before the CurrencyMiddleware from toraan/currency:

/**

* Handle an incoming request.

*

* @param \Illuminate\Http\Request $request

* @param \Closure $next

* @return mixed

*/

public function handle(Request $request, Closure $next)

{

if (! $request->get('currency') &&

! $request->getSession()->get('currency')) {

$clientIP = $request->getClientIp()



$localCurrency = geoip($clientIP)->getAttribute('currency');



$request->getSession()->put([

'currency' => $localCurrency,

]);

}



return $next($request);

}

Displaying the localised pricing

So we can now bring all of this together by displaying localised pricing and having it conveniently formatted for us.

{!! currency($plan->price / 100, 'USD', strip_tags(currency()->getUserCurrency())) !!}

Of course, it’s not stricly necessary to call strip_tags() when retrieving the users’ currency, because any invalid currency codes are rejected by the CurrencyMiddleware, even if initially set within your own middleware. Alas, I’ll leave the choice with you.

About me

My name is Ben Stones. I lead Sprint Boards®, an online retrospective tool for Agile developers, providing distributed teams with the tools they need to discuss and collaborate together from anywhere.