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 Swap Laravel Package

florianv/laravel-swap

Laravel Swap lets you convert currencies and fetch exchange rates in Laravel apps. It wraps the Swap PHP library, supporting multiple rate providers, caching, and simple configuration for reliable currency conversion.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and forget the boilerplate. composer require florianv/laravel-swap — the service provider and Swap facade are auto-discovered, so nothing manual is needed. Publish the config once:

php artisan vendor:publish --provider="Swap\Laravel\SwapServiceProvider"

Check your Laravel version first. As of 2.5.0 the package requires Laravel 11, 12 or 13 — Laravel 10 support was dropped and Lumen is no longer tested. If you're on 10 or older, stay on 2.4.x deliberately (see the version gotcha below). 2.5.0 also bumps florianv/swap to ^4.6, which pulls in Exchanger 2.11's provider fixes — that combination is what makes the provider list in config/swap.php accurate, so don't pin the inner package lower.

Open two files before writing any code:

  1. config/swap.php — providers, fallback order, cache TTL, cache store, HTTP client. Everything you tune happens here. In 2.5.0 this file was corrected to use the real option names for every provider, so copy keys from it verbatim rather than from a blog post.
  2. vendor/florianv/swap/src/Provider/ — the list of providers you can drop in (Fixer, CurrencyLayer, OpenExchangeRates, ExchangeRatesApi, UniRateAPI, EuropeanCentralBank, NationalBankOfDenmark, …), each with its own required options.

Put an API key in .env. Most commercial providers need one. If you have none, EuropeanCentralBank or NationalBankOfDenmark are keyless but only give daily rates and a limited currency list — fine for display-only use cases, not for anything transactional.

First use case — a 3-line conversion:

use Swap\Laravel\Facades\Swap;

$rate = Swap::latest('EUR/USD');       // Rate object
$value = $rate->getValue();            // float: USD per 1 EUR
$price = 100 * $value;                 // €100 in USD

Or in tinker: php artisan tinker --execute="dump(Swap::latest('EUR/USD')->getValue());". If that returns a number, the package is configured correctly and you can move on to real integration.

Where to look next: the facade Swap\Laravel\Facades\Swap, then the Swap\Builder class (this is what Swap::create() returns — it's your escape hatch for anything the config doesn't cover).

Implementation Patterns

Inject the contract, not the facade

The facade is convenient, but for anything beyond a controller the cleaner pattern is constructor injection of Swap\Swap (the interface). It makes unit-testing trivial and reads better:

class PricePresenter
{
    public function __construct(private \Swap\Swap $swap) {}

    public function convert(float $amount, string $from, string $to): float
    {
        return $amount * $this->swap->latest("{$from}/{$to}")->getValue();
    }
}

The container resolves this automatically — no binding needed.

Custom builder when the config isn't enough

Swap::create() returns a builder seeded from config/swap.php. Use it when a single feature needs a different provider, cache, or HTTP client than the app-wide default:

$swap = Swap::create()
    ->addProvider('ecb', ['class' => \Swap\Provider\EuropeanCentralBank::class])
    ->useCache(\Illuminate\Support\Facades\Cache::store('redis'), 300)
    ->build();

Good for CLI commands that must not touch the production cache, or for one-off jobs with stricter TTLs.

Fallback chain in config

'fallback_providers' => ['fixer', 'exchangerate_host', 'european_central_bank'],

Providers are tried in order — first success wins. Put your most reliable provider first, not the cheapest. The fallback is what saves you when the primary hits a rate limit at 02:00 UTC and your queue starts failing.

Cache at the right layer

Configure TTL inside each provider block (or globally) so the package handles it. Do not also wrap Swap::latest() in Cache::remember() — you'll create a stale-cache-on-top-of-stale-cache problem that's very hard to debug. Pick one layer.

Rule of thumb: display prices → 1 h TTL; checkout / invoice → 5–15 min; dashboards → 60 s; anything trading-adjacent → don't cache, and don't use this package.

Note that swap.cache, swap.http_client and swap.request_factory in the config are service names, not instances — 2.5.0 makes that explicit by throwing InvalidArgumentException if you hand it anything else. So 'cache' => 'redis' or your container binding key, never Cache::store('redis').

Historical rates for invoices and reports

$rate = Swap::historical('EUR/USD', $invoice->created_at);

Always pass the timestamp explicitly (Carbon::parse($invoice->created_at)) rather than relying on "now" logic — otherwise re-rendering an old invoice silently changes the numbers. Not every provider has full history; check which one you've configured before promising users multi-year back-conversions.

Handle inversion explicitly

If your provider only publishes EUR-quoted pairs but you need USD/EUR, use:

$inverse = Swap::latest('EUR/USD')->getInv();

It's not just 1 / value — it preserves the rate metadata. Do this instead of hand-math.

Request-scoped memoization

A page that lists 40 products calls the provider once per product if you're careless. Memoize per request:

class RateRegistry
{
    private array $rates = [];

    public function __construct(private \Swap\Swap $swap) {}

    public function rate(string $pair): float
    {
        return $this->rates[$pair] ??= $this->swap->latest($pair)->getValue();
    }
}

Register it as a singleton only in the request lifecycle (or use scoped()). This alone eliminates most rate-limit complaints.

Warm the cache on a schedule

A dedicated php artisan swap:warm command that loops over the currencies your app actually supports and calls latest() for each, scheduled every 30 minutes, is worth more than any TTL tuning. Users then read from cache and the provider never sees a spike.

// app/Console/Commands/WarmRatesCommand.php
foreach (config('app.supported_currencies', ['USD', 'GBP', 'JPY']) as $quote) {
    Swap::latest("EUR/{$quote}");
}

Testing

Mock the facade, not the provider:

Swap::shouldReceive('latest')
    ->with('EUR/USD')
    ->andReturn(\Swap\Model\Rate::create(1.10, new \DateTime(), 'EUR', 'USD'));

Or bind a fake Swap\Swap implementation in the container — usually simpler if you're injecting the contract. Either way, never let the test hit a live API.

Integration shape

  • Blade/Inertia: format via a Money value object — don't sprinkle float multiplication across templates.
  • FormRequest: validate currency codes against a hardcoded ISO list in config, not against the swap provider. The provider's currency list is not stable enough to be your validator.
  • API responses: cache the rate on the response object (e.g. rate_used, rate_date) so downstream consumers can audit.

Gotchas and Tips

2.5.0 is a breaking upgrade for older apps. Laravel 11/12/13 only, Lumen no longer tested, and the unused php-cache fallback in the service provider is gone. If any of your bindings relied on that fallback (e.g. you swapped the cache implementation expecting the package to degrade gracefully), verify explicitly. Read the changelog before bumping in a legacy app — this is a major-requirements drop in a minor version.

Option names were wrong before 2.5.0 — and now they're right. If you copied a config snippet from an older tutorial, you may be passing access_key where APILayer services (Fixer, CurrencyLayer, …) expect api_key. The provider won't complain loudly; it just won't work. Diff your published config/swap.php against the 2.5.0 version after upgrading and re-check every key.

Provider docs now have specific plan caveats. exchange_rates_api on the free plan is EUR-based — you can't request arbitrary base currencies, so plan your pair orientation around EUR or pay up. coin_layer sends your API key over plain HTTP unless paid is true — that's a credential leaking across the wire, so if you're using CoinLayer, treat it as an internal-only, non-sensitive integration or upgrade.

Pass names, not objects, to swap.cache / swap.http_client / swap.request_factory. 2.5.0 added an explicit InvalidArgumentException for non-name values. Before, you'd get a confusing failure deep in the resolver; now you get a clear one, but the fix is still to put a string/binding key in config.

Wrong pair orientation causes silent 1/x bugs. Swap::latest('EUR/USD') means "1 EUR = X USD". Forgetting this is the single most common integration bug in this package. Write a test asserting a known-good rate.

Swap::latest() throws on failure. Every provider failure raises Swap\Exception\* — always wrap in try/catch and fall back to a stored rate. A page that 500s because a currency API is down is worse than a page with yesterday's rate and a note.

Do not trust the facade for mocking Swap::create(). create() returns a builder and is awkward to mock. If you need a custom-built swap in tests, inject the builder or the built instance rather than reaching for the facade.

Fallback failure is silent by default. Wire a log line in when you catch the exception — otherwise you'll discover months later that the primary provider has been dead the whole time and the fallback has been serving slightly wrong rates.

Config cache bites in development. After editing config/swap.php, run php artisan config:clear. In production, remember php artisan config:cache — the config file is not read on each request, so env-var changes require a rebuild.

API keys are required even on "free" providers. ExchangeRatesApi's free tier still needs a key. Only EuropeanCentralBank, NationalBankOfDenmark and a couple of other national-bank providers are genuinely keyless — and they are daily-only, not real-time.

Historical availability varies wildly. A provider that gives you a year of daily EUR/USD might have zero history for exotic pairs. If you promise "invoice in currency of customer's choice at the time of order", validate the pair at order time by calling historical() once and caching the success.

Precision matters for money. Rate::getValue() returns a float. For invoice amounts over ~$10,000 you'll hit rounding drift. Convert to bcmath or a Money object immediately after the swap call — don't accumulate float math downstream.

Cache key collisions aren't obvious. The package writes cache keys with its own prefix. If you're debugging "why is the rate stale", check the provider's cache config first, then Laravel's Cache::get. They're different layers.

Fallback ordering is priority ordering. Listing the cheapest provider first is a mistake if it has a 20-request/min rate limit. Order by reliability, not cost.

Custom provider is the cleanest extension point. If your company has an internal FX microservice, implement Swap\Provider\ProviderInterface (a fetch method returning a Rate) and register it in config/swap.php under providers. The fallback chain and cache then apply automatically — you don't have to reimplement that logic. The provider list grew again in 2.5.0 (UniRateAPI, National Bank of Denmark) and a test now keeps the documented providers in sync with Exchanger, so check the list before writing a provider that already exists.

Package version tracks Laravel major. Check composer.json constraints before upgrading Laravel. Version 2.5.0 targets Laravel 11–13 and drops 10; 2.4.x is the last line for Laravel 10. Pin intentionally rather than letting a ^2 constraint drag you across a Laravel baseline change.

Don't call the provider inside a database migration or seeder that might run on a fresh environment offline. It will fail the migration pipeline. Seed a fixed rate constant instead.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource