A driver-based currency conversion package for Laravel with caching.
bernskiold/laravel-currency-converter is a Laravel package for a driver-based currency conversion package for laravel with caching..
It currently has 0 GitHub stars and 2 downloads on Packagist (latest version 1.0.0).
Install it with composer require bernskiold/laravel-currency-converter.
Discover more Laravel packages by bernskiold
or browse all Laravel packages to compare alternatives.
Last updated
Working with money in more than one currency shouldn't be painful. This package gives you a clean, expressive way to convert between currencies, look up exchange rates, and keep a base-currency copy of your model's amounts in sync — all backed by pluggable providers and sensible caching.
use Bernskiold\LaravelCurrencyConverter\Facades\CurrencyConverter;
CurrencyConverter::convert(100, 'USD', 'SEK'); // 1050.0
CurrencyConverter::rate('USD', 'SEK'); // 10.5
CurrencyConverter::toBase(100, 'USD'); // into your app's base currency
It ships with a free, keyless provider out of the box, so you can be up and running in a minute — and when you're ready for something else, swapping providers is a one-line change.
CurrencyConversionException, so you decide how to degrade — never the library.You can install the package via Composer:
composer require bernskiold/laravel-currency-converter
That's it — the free Frankfurter driver is active by default. If you'd like to tweak the providers, caching, base currency, or number formatting, publish the config:
php artisan vendor:publish --tag=currency-converter-config
Reach for the facade anywhere in your app:
use Bernskiold\LaravelCurrencyConverter\Facades\CurrencyConverter;
// Convert a value (rounded to the configured number of decimals).
CurrencyConverter::convert(100, 'USD', 'SEK'); // 1050.0
// Just the rate, please.
CurrencyConverter::rate('USD', 'SEK'); // 10.5
// Convert into — or out of — your configured base currency.
CurrencyConverter::toBase(100, 'USD'); // USD -> base_currency
CurrencyConverter::fromBase(100, 'USD'); // base_currency -> USD
// Need a specific provider for a single call? Say so.
CurrencyConverter::convert(100, 'USD', 'SEK', driver: 'exchangerate_host');
Prefer dependency injection over facades? Resolve the class straight from the container — same methods, no facade required:
app(\Bernskiold\LaravelCurrencyConverter\CurrencyConverter::class)->convert(100, 'USD', 'SEK');
When it's time to show a number to a human, format() uses your configured formatting (US conventions by default):
CurrencyConverter::format(1234.5); // "1,234.50"
CurrencyConverter::format(1234.5, 'USD'); // "1,234.50 USD"
Often you'll store an amount in whatever currency the record was created in, but you also want a copy of that amount in your reporting currency so totals and comparisons are easy. The ConvertsCurrencies trait keeps that copy in sync for you — every time the model is saved.
Add the trait and tell it which columns to convert with a $currencyConversions map. Each entry maps a source column (the amount in the record's own currency) to a target column (where the base-currency value should be stored):
use Bernskiold\LaravelCurrencyConverter\Concerns\ConvertsCurrencies;
class Expense extends Model
{
use ConvertsCurrencies;
protected static array $currencyConversions = [
'amount' => 'amount_sek',
];
}
You can convert as many columns as you like — just add more entries to the map.
The trait makes a few small assumptions:
A currency column. It reads the record's currency from a currency attribute by default (an ISO code such as USD).
The source and target columns exist. Both the amount column and its base-currency counterpart must be real database columns. A migration for the example above would look like:
$table->string('currency', 3)->default('SEK');
$table->decimal('amount', 12, 2)->nullable();
$table->decimal('amount_sek', 12, 2)->nullable();
That's it — no other configuration is required on the model.
If your currency lives somewhere other than a currency column, override currencyColumn():
protected function currencyColumn(): string
{
return 'currency_code';
}
Need the currency from somewhere that isn't a plain column — a relationship, say? Override currencyCode() instead, which is what the trait actually calls (and which is public, so it's handy in your own code too):
public function currencyCode(): ?string
{
return $this->billingAccount->currency;
}
On create and update, each target column is filled using toBase() (on update, only when the amount or currency actually changed).
If the record is already in the base currency, the amount is copied across as-is — no API call.
If a conversion ever fails, it's logged rather than thrown, so the save always goes through. You can fill in any gaps later:
$expense->recalculateCurrencyConversions();
The trait also gives your views a couple of friendly helpers, both formatted with your configured number formatting:
$expense->amountWithCurrency('amount'); // "1,234.56 USD"
$expense->amountInBaseCurrency('amount'); // "12,962.88 SEK"
Set your default provider in config/currency-converter.php (or via the CURRENCY_CONVERTER_DRIVER environment variable):
| Driver | Key required | Notes |
|----------------------|--------------|-----------------------------------------------------------------------|
| frankfurter | No | ECB mid-market reference rates, updated daily. The default. |
| exchangerate_api | Optional | Broad coverage. Uses your key if set (EXCHANGERATE_API_KEY), otherwise the free keyless endpoint. |
| exchangerate_host | Yes | Set EXCHANGERATE_HOST_KEY. |
| open_exchange_rates| Yes | Set OPEN_EXCHANGE_RATES_APP_ID. Free plan is USD-base only. |
| fixer | Yes | Set FIXER_ACCESS_KEY. Free plan is EUR-base only. |
| database | No | Read rates from a table you manage. See below. |
| fixed | No | Static rates from config — great for tests. |
The database driver reads rates from a table you control — handy when you need pinned, auditable rates rather than live market data. Publish and run the migration:
php artisan vendor:publish --tag=currency-converter-migrations
php artisan migrate
This creates an exchange_rates table (from_currency, to_currency, rate). The table and column names are configurable under currency-converter.drivers.database.
A convenience ExchangeRate model is included for managing the rates — from a scheduled job, an importer, or by hand:
use Bernskiold\LaravelCurrencyConverter\Models\ExchangeRate;
ExchangeRate::setRate('USD', 'SEK', 10.42); // creates or updates the pair
ExchangeRate::forPair('USD', 'SEK')->value('rate'); // 10.42
The model reads its table, connection, and column names from the same config, so it stays in step with the driver.
Need rates from somewhere we don't support yet? Write a class that implements the ExchangeRateProvider contract:
namespace App\CurrencyConverter;
use Bernskiold\LaravelCurrencyConverter\Contracts\ExchangeRateProvider;
use Illuminate\Support\Facades\Http;
class AcmeBankDriver implements ExchangeRateProvider
{
public function getRate(string $from, string $to): float
{
return (float) Http::acmeBank()
->get("/rates/{$from}/{$to}")
->json('rate');
}
}
Then register it — usually in a service provider's boot() method — and select it via config (currency-converter.default) or per call (driver: 'acme-bank'):
use Bernskiold\LaravelCurrencyConverter\Facades\CurrencyConverter;
use App\CurrencyConverter\AcmeBankDriver;
CurrencyConverter::extend('acme-bank', fn () => new AcmeBankDriver);
The closure receives the container, so feel free to resolve any dependencies your driver needs.
The easiest way to test code that converts currencies is to fake the converter. CurrencyConverter::fake() swaps in a fake that returns predictable rates and never touches the network — and records every conversion so you can assert against it:
use Bernskiold\LaravelCurrencyConverter\Facades\CurrencyConverter;
CurrencyConverter::fake(['USD' => ['SEK' => 10.0]]);
// ... exercise your code ...
CurrencyConverter::assertConverted('USD', 'SEK');
Any currency pair you don't define converts 1:1, so a bare CurrencyConverter::fake() is enough when you only care that conversion happened. The fake also drives the ConvertsCurrencies trait, so your models behave exactly as they would in production — without an HTTP call in sight.
A few assertions are available on the fake:
CurrencyConverter::assertConverted('USD', 'SEK'); // a USD -> SEK conversion happened
CurrencyConverter::assertConverted(); // any conversion happened
CurrencyConverter::assertConverted(fn ($value, $from, $to) => $value === 100.0);
CurrencyConverter::assertConvertedTimes(2, 'USD', 'SEK');
CurrencyConverter::assertNothingConverted();
Prefer to exercise the real conversion path? Reach for the fixed driver (or Http::fake() with the HTTP providers) to stay fast and offline:
config()->set('currency-converter.default', 'fixed');
config()->set('currency-converter.drivers.fixed.rates', ['USD' => ['SEK' => 10.0]]);
expect(CurrencyConverter::convert(100, 'USD', 'SEK'))->toBe(1000.0);
You can run the package's own test suite with:
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see the License File for more information.