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

Exchanger Laravel Package

florianv/exchanger

PHP exchange rate provider layer exposing 30+ services (fastFOREX, ECB, national banks) through one ExchangeRateService interface, with chain fallback, PSR-16 caching and historical rates. Maintained since 2016; requires PHP 8.2+.

View on GitHub
Deep Wiki
Context7

Getting Started

Exchanger is the provider layer underneath florianv/swap: it gives you 30+ exchange-rate providers, a fallback chain, and historical rates behind one ExchangeRateService interface. It does not ship a Laravel service provider, so a Laravel integration is three steps: install, bind a singleton, resolve it in your own converter service.

1. Install the driver stack. Exchanger needs a PSR-18 client plus a PSR-17 request factory, discovered via php-http/discovery:

composer require florianv/exchanger symfony/http-client nyholm/psr7

2. Put credentials in config, never getenv() in code (Laravel's config:cache breaks raw env() calls outside config files):

// config/exchange.php
return [
    'fastforex_key' => env('FASTFOREX_API_KEY'),
    'cache_ttl'     => (int) env('EXCHANGE_CACHE_TTL', 3600),
];

3. Bind an Exchanger singleton wrapping a Chain so a dead provider degrades instead of 500-ing your checkout:

// app/Providers/ExchangeServiceProvider.php
use Exchanger\Exchanger;
use Exchanger\Service\{Chain, FastForex, EuropeanCentralBank};

public function register(): void
{
    $this->app->singleton(Exchanger::class, function () {
        $service = new Chain([
            new FastForex(null, null, ['api_key' => config('exchange.fastforex_key')]),
            new EuropeanCentralBank(), // free, EUR-base only, daily granularity
        ]);

        return new Exchanger($service);
    });
}

First use case: convert an order total and record provenance (the rate, its date, and which provider served it) so finance can audit later:

use Exchanger\ExchangeRateQueryBuilder;

$query = (new ExchangeRateQueryBuilder('EUR/USD'))->build();
$rate  = app(Exchanger::class)->getExchangeRate($query);

$rate->getValue();        // float, e.g. 1.0823 — never a string
$rate->getDate();         // DateTime of the quote
$rate->getProviderName(); // 'fastforex' or 'european_central_bank'

$totalUsd = round($order->total_eur * $rate->getValue(), 2);

Where to look first: the README quickstart, then doc/readme.md — it holds the per-service configuration reference (each provider's constructor options differ), cache options, and error handling. Read Exchanger\Service\Registry to see the identifiers you can drive from config.

Implementation Patterns

Wrap the package, don't scatter it. Expose one CurrencyConverter class that takes an Exchanger, memoizes rates per request (a chain call is a network round-trip; a loop over 50 orders should hit it once), and returns a small DTO carrying value, date, and provider.

final class CurrencyConverter
{
    private array $memo = [];

    public function __construct(private \Exchanger\Exchanger $exchanger) {}

    public function convert(float $amount, string $from, string $to): ConvertedAmount
    {
        $pair  = "{$from}/{$to}";
        $query = (new \Exchanger\ExchangeRateQueryBuilder($pair))->build();

        $rate = $this->memo[$pair] ??= $this->exchanger->getExchangeRate($query);

        return new ConvertedAmount(
            round($amount * $rate->getValue(), 2),
            $rate->getValue(),
            $rate->getProviderName(),
            $rate->getDate(),
        );
    }
}

Config-driven chains. Make the provider list a config array of [identifier, options] and build the chain in the service provider by resolving each identifier out of Exchanger\Service\Registry. That way staging can run ECB-only at zero cost while production runs fastFOREX → ECB, with no code change.

Cache aggressively — this is the whole point of the package. Rates move on the order of minutes for commercial providers and once a day for central banks. Pass cache options per query (keys like cache, cache_ttl, cache_key — verify exact names in doc/readme.md for your pinned version) and inject Laravel's PSR-16-compatible cache.store. Set TTL by use case: 60–300s for a live pricing widget, 6–24h for historical/back-office conversion, and a stable explicit cache_key when you want the same pair reused across requests.

Historical rates via the same builder. ->setDate($date) (not a separate class — the old Historical service was removed in 2.0). This is the pattern for revaluing past invoices or reconciling payouts at the rate effective on the transaction date.

Use Chain for resilience, but log what it swallows. Unsupported pairs are skipped silently by design; only actual errors propagate, aggregated into a ChainException if every provider fails. Wrap getExchangeRate() in a try/catch and report the ChainException with all nested exceptions — otherwise provider outages are invisible until a customer complains.

Custom providers implement Exchanger\Contract\ExchangeRateService; add the SupportsHistoricalQueries trait if you need setDate() to work. A thin decorator is often better than a new provider: wrap any service to add rate limiting, metrics, request signing, or a database-backed fallback.

Testing. Point the chain at the in-memory PhpArray service in your test suite (it only accepts scalar rates — don't feed it rate objects). Assert on getProviderName() too, so a chain reorder can't silently change behaviour.

Gotchas and Tips

  • PSR-18 discovery bites in CI. composer require florianv/exchanger alone gives you no HTTP client; you need symfony/http-client + nyholm/psr7 (or an explicit client injected into the service constructor). Missing clients surface as a Psr18ClientDiscovery exception at runtime, not at composer time. 2.9.0 swapped HttpClientDiscovery for Psr18ClientDiscovery, so old HTTPlug-era snippets no longer apply — pin ^2.9.
  • Floats are not money. getValue() is deliberately a float (changed in 2.0 from string). Multiply, round once at the end, and keep the stored rate alongside the converted amount. If you need exact minor-unit arithmetic, convert via integer cents around the multiplication.
  • ECB is EUR-base and weekday-only. It publishes once per business day, so EUR/USD works, USD/EUR needs cross-rate math, and a Saturday setDate() may throw or return the prior business day depending on provider. Always have a commercial provider first in the chain for non-EUR pairs.
  • The provider that answered may not be the one you expect. On a chain, a transient primary failure means the fallback's rate (and date) is returned. Persist getProviderName() and getDate() with every conversion in anything financial — it's your audit trail.
  • Free tiers are USD- or EUR-base restricted. Several commercial providers lock the base currency on the free plan while allowing any quote; a "working" pair flips to ChainException only when you hit the constrained base. The provider table in the README marks these.
  • Don't call env() at runtime. Read keys through config() so php artisan config:cache doesn't silently null out your API key.
  • Chain cost doubles on failure. A dead primary plus a fallback means two HTTP round-trips per uncached pair. Cache before you chain: cache misses amplify outages.
  • ExchangeRate is immutable with getCurrencyPair(), getValue(), getDate(), getProviderName(). Don't try to mutate a rate — build a new DTO for anything you want to store or transform.
  • Small ecosystem, own your integration. 185 stars and no dependents means few battle-tested Laravel examples; florianv/laravel-swap exists but is a wrapper over Swap, not Exchanger, and tends to lag Laravel releases. For a modern Laravel app, wiring Exchanger directly as shown is usually less risk than adopting the older binding.
  • Extension point for observability: decorate the service (or add a Guzzle/Symfony HttpClient middleware) rather than editing providers — you get latency and error metrics per provider without forking the package.
  • Version-pin and check the changelog before upgrading. Releases routinely change provider endpoints (BNR moved to curs.bnr.ro in 2.9.0) and drop PHP support; an unpinned composer update can hand you a broken provider on a Friday.
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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk