Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Exchange Rates Laravel Package

ashallendesign/laravel-exchange-rates

A Laravel package for fetching live exchange rates and converting between currencies. It offers a clean, cache-friendly API with multiple drivers, historical rate support, validation rules for currency codes, and built-in caching, making currency conversion easy to add to any Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

Check your platform version before you install. As of v7.11.0 the package requires PHP 8.2+ and supports Laravel 13 (alongside the previous majors). PHP 8.0 and 8.1 support was dropped in that release, so pin "ashallendesign/laravel-exchange-rates": "^7.10" if you're still on an older runtime — otherwise Composer will fail resolution with a php constraint conflict that reads like a package bug rather than a platform mismatch.

Install and publish config:

composer require ashallendesign/laravel-exchange-rates
php artisan vendor:publish --tag="exchange-rates-config"

Then set your driver credentials in .env:

EXCHANGE_RATES_DRIVER=exchangeratesapi
EXCHANGE_RATES_API_KEY=your-key-here

The package registers a facade, so the first useful call is a one-liner anywhere in your app:

use AshAllenDesign\LaravelExchangeRates\Facades\ExchangeRate;

$gbp = ExchangeRate::convert(100, 'USD', 'GBP'); // float, e.g. 78.42

Where to look first:

  1. config/exchange-rates.php — the default driver, each driver's api_key, and the cache block. This is where 90% of your integration decisions live.
  2. AshAllenDesign\LaravelExchangeRates\Facades\ExchangeRate — the entire public surface is convert(), rate(), rates(), currencies(), plus the chainable driver() / shouldCache() / shouldNotCache().
  3. The package's src/Drivers/ directory — tells you exactly which upstream endpoints and status codes are handled, and what to add if you need a provider it doesn't ship.

First real use case: a multi-currency price display. Take a base price stored in your DB, call convert() once per page render (or better, per cached rate), and format with your existing money helper. Do not build this as a tight loop — see the rates() pattern below.

Before you ship, verify your plan's base-currency support. The common free tier of exchangeratesapi.io historically pins the base currency to EUR, so a straight USD → GBP conversion returns a provider error even though the code is fine. If you hit that, switch to the exchange_rate_host driver or upgrade the plan.

Implementation Patterns

Wrap it in a single service, never call the facade from controllers. This is the highest-leverage change you can make: it gives you one place to add fallbacks, logging, and caching, and one place to swap drivers.

final class CurrencyConverter
{
    public function convert(float $amount, string $from, string $to, ?Carbon $date = null): float
    {
        return ExchangeRate::convert($amount, strtoupper($from), strtoupper($to), $date);
    }
}

Bulk-fetch rates, then compute locally. rates() gives you a whole base-currency table in one HTTP round trip. If a page shows prices in five currencies, that's one call, not five:

$rates = ExchangeRate::rates('USD', ['GBP', 'EUR', 'AUD']);

$prices = collect($rates)->map(fn ($rate) => round($basePrice * $rate, 2));

Snapshot rates into your own table via a scheduled command. Relying on the API at request time means every page view is coupled to a third party's uptime and your rate limit. Land rates in exchange_rates (base, target, rate, fetched_at) on a schedule and read from your DB in the hot path:

// app/Console/Commands/SyncExchangeRates.php
$rates = ExchangeRate::rates('USD', ['GBP', 'EUR', 'AUD']);

foreach ($rates as $code => $rate) {
    ExchangeRateSnapshot::updateOrCreate(
        ['base' => 'USD', 'target' => $code],
        ['rate' => $rate, 'fetched_at' => now()],
    );
}

Store the rate on the order, not just the converted total. For anything financial — invoices, checkout, refunds — persist rate_used and rate_fetched_at alongside the amount. When a customer disputes a charge three weeks later, you need the rate that was true then, and re-querying the API for a historical date is both slower and not guaranteed to match.

$order->update([
    'total'        => round($baseTotal * $rate, 2),
    'rate_used'    => $rate,
    'rate_base'    => 'USD',
    'rate_fetched_at' => now(),
]);

Validate user-supplied currencies against currencies(). Cache the result for a day and use it in a Rule rather than hardcoding a list or trusting a string|max:3 rule:

Rule::in(Cache::remember('currencies', now()->addDay(), fn () => ExchangeRate::currencies()))

Mock the facade in tests — but only the facade. The package's HTTP calls are real by default, so unit tests should stub the facade:

ExchangeRate::shouldReceive('convert')->once()->andReturn(0.79);

For feature tests that genuinely exercise the driver, pair this with Http::fake() and assert against a recorded fixture. Prefer the facade mock for anything business-logic adjacent; you don't want CI failing because a fixture drifted upstream.

Use the driver chain as your test/prod switch. Point EXCHANGE_RATES_DRIVER at a different provider per environment so a sandbox deployment can't burn your production quota, and so a provider outage has a one-line fix.

Gotchas and Tips

Pin the major and read the platform constraint on every bump. v7.11.0 is a platform release, not a feature release: PHP 8.0/8.1 dropped, Laravel 13 added. If you're mid-upgrade on an older runtime, constrain to ^7.10 deliberately rather than letting composer update strand you on a version your CI matrix can't install. Conversely, if you're planning a Laravel 13 upgrade, this package is no longer a blocker — but only from 7.11.0 onward.

Caching is on by default and it will surprise you. The same convert() arguments return the cached value until the TTL expires, which is exactly what you want at scale and exactly what confuses you when you're debugging "why is the rate wrong." Chain ->shouldNotCache() when you need a fresh read, and check the cache block in the config before you file a bug against your own code.

Rate limits are the real design constraint. Free tiers are on the order of ~100 requests/month for exchangeratesapi.io. Any per-request call pattern will exhaust that in an afternoon of traffic. Cache aggressively, snapshot on a schedule, and never put an API call inside a model accessor or a Blade loop.

convert() returns a raw float. There is no Money object, no rounding policy, and no currency-aware decimal handling. Round explicitly at the boundary (round($x, 2) for display, integer minor units for storage) or you will accumulate float drift across a cart. Don't do arithmetic on converted values repeatedly —

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin