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

Swap Laravel Package

florianv/swap

Swap is an easy-to-use PHP currency conversion library that retrieves exchange rates from 30+ providers behind a single API, with PSR-16 caching, historical rates, and automatic provider fallback. PHP 8.2+, maintained since 2014.

View on GitHub
Deep Wiki
Context7

Getting Started

Install. For a plain PHP service, pull in Swap plus a PSR-18 client and PSR-17 factory — Swap deliberately ships neither:

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

Inside a Laravel app the pragmatic route is the framework wrapper, which wires the builder, config and facade for you:

composer require florianv/laravel-swap

It auto-registers a service provider, exposes a Swap facade and publishes config/swap.php where you declare the provider chain (service identifier + options). Either way you need PHP 8.2+ — check composer.json before you start on a legacy 8.1 app.

Where to look first, in order:

  1. The README quickstart — it gets you a working $swap->latest('EUR/USD') in one snippet.
  2. doc/readme.md → Configuration → How the fallback chain works, then Caching. These two sections determine your production behaviour more than anything else.
  3. The provider tables. This is the real decision point: pick a provider that supports arbitrary base currencies (fastFOREX, Fixer paid, Open Exchange Rates paid) if you need USD→JPY, GBP→CHF, etc. Public bank feeds are almost all single-currency-anchored (ECB is EUR-base, national banks are local-quote) and will silently skip pairs they can't answer.

First use case — a single conversion in a pricing helper. Don't sprinkle (new Builder()) through your codebase; build once, inject everywhere:

// config/swap.php (laravel-swap)
'providers' => [
    ['service' => 'fastforex', 'options' => ['api_key' => env('FASTFOREX_API_KEY')]],
    ['service' => 'european_central_bank'], // free fallback for EUR pairs
],

// app/Services/Fx.php
public function rate(string $base, string $quote): float
{
    $rate = Swap::latest("{$base}/{$quote}");   // 'EUR/USD' => USD per 1 EUR
    return $rate->getValue();
}

public function convert(float $amount, string $base, string $quote): float
{
    return $amount * $this->rate($base, $quote);
}

Note the direction: latest('EUR/USD') answers "how many USD for one EUR". Swap never inverts and never multiplies — the arithmetic is yours. Persist getProviderName() and getDate() alongside anything financial; you will want them in the audit trail.

Implementation Patterns

1. Bind the built instance as a singleton, not the builder. Constructing Swap per call re-parses config and re-resolves HTTP client discovery. In Laravel, register the built Swap (or your Fx wrapper) in a service provider and inject the interface. This also gives you one obvious place to swap the client in tests.

2. Bridge Laravel's cache into Swap's PSR-16 slot. Illuminate\Cache\Repository already implements Psr\SimpleCache\CacheInterface, so you get Redis/database/file caching for free:

$swap = (new Builder())
    ->add('fastforex', ['api_key' => config('services.fastforex.key')])
    ->useCache(Cache::store('redis'))
    ->build();

Prefer this over hand-rolling a cache layer — Swap keys per provider/pair/date, which is hard to reproduce correctly.

3. Always configure a chain, and order it by cost. Primary commercial provider first (rich pair coverage, real-time), then a free public feed as a safety net, e.g. fastforex → european_central_bank. Keep in mind the chain retries on errors, so a dead primary adds a full HTTP timeout to every request — set a per-query timeout/verify_ssl style option rather than accepting the default when latency matters.

4. Snapshot the rate at transaction time. For invoices, orders and ledgers, never recompute later: store amount_base, amount_quote, rate_value, rate_date, rate_provider on the row. Use historical($pair, $date) for backfill/reconciliation of past periods, and treat the stored snapshot as authoritative even if the provider later revises the figure.

5. Cache-warm on a schedule. A rate is not request-scoped data. Add a scheduled command that fetches your handful of real currency pairs every 15–60 minutes into the cache; web requests then hit cache almost always, and free-tier request quotas stop mattering. Same idea for historical backfill jobs, which should run in a queue with retries.

6. Test with a mock PSR-18 client, not mocks of Swap. Because useHttpClient() accepts any PSR-18 client, symfony/http-client's MockHttpClient with recorded provider JSON lets you exercise the real parser, the real fallback chain and the real cache — a much better signal than stubbing getValue(). For pure domain tests, implement ExchangeRateService with a fixed-rate stub and register it via addExchangeRateService(); that's also the extension point for a bespoke internal rates service.

7. Use the moneyphp adapter for anything that touches accounting. Swap returns a float. For totals, tax and rounding, feed rates into moneyphp/money via the SwapExchange adapter (or parse the float into a decimal string with bcmath/brick/math) rather than chaining float multiplications.

8. Display-layer vs. settlement-layer separation. Storefront price display can tolerate a cached rate a few hours old; settlement cannot. Model them as two methods on your Fx service — displayRate() (cached, forgiving, falls back to last known) and settlementRate() (short TTL, no fallback to stale data, throws loudly). One config should not serve both.

Gotchas and Tips

The HTTP client is not bundled — and Composer will fight you. After installing a client you may hit the php-http/discovery plugin prompt. Add it explicitly:

"config": { "allow-plugins": { "php-http/discovery": true } }

Without it, auto-discovery silently finds nothing and you get a confusing "no client" error at runtime rather than install time.

Unsupported pairs are skipped silently. Only when every provider fails do you get a ChainException. That means a typo in a currency code, or asking the ECB for USD/JPY, looks identical to a network outage from the caller's perspective. Catch the exception, iterate the collected errors (getErrors()), and separate "unsupported pair" from "provider down" — they need different remediation and different alerting.

No automatic cross-conversion. If your chain is EUR-anchored only, GBP/CHF will fail even though both legs exist. Either pay for a provider with arbitrary base support, or implement cross-rates yourself (GBP/EUR × EUR/CHF) with explicit rounding rules — and be aware that compounding two rates doubles your error and your latency.

Public feeds have business-day granularity. ECB publishes once per working day, around 16:00 CET, and not on weekends or TARGET holidays. A historical() call for a Saturday will not give you a Saturday rate. Implement a "walk back to the previous business day" helper once, in your Fx service, rather than in five call sites.

Timezones on historical dates. The date you pass is interpreted by the provider. A rate for "2024-01-15" from a Tokyo-anchored feed versus an ECB feed may not cover the same trading window. For anything reconciled, store the provider name alongside the rate so a reviewer can explain a one-day discrepancy.

Floats and precision. getValue() is a float; six-ish decimals is typical for majors and much worse for exotics. Never round-trip money through it more than necessary, and define your display rounding in one place — the rate itself should not be pre-rounded, or your totals drift.

Cache key collisions across config changes. If you switch primary providers or change a cache TTL, the underlying keys shift and you can be quietly serving stale numbers from an old provider (or missing cache entirely for a while after deploy). Use an explicit per-query cache_key/TTL override for the pairs that matter, and flush deliberately on provider cutover.

Don't call it in a loop. The classic multi-currency storefront bug: rendering a product grid and issuing one latest() per row. Even with caching this is wasteful and, on a cache miss storm, will blow through free-tier quotas in seconds. Fetch the distinct pairs once per request/schedule, or warm them ahead of time.

API keys belong in env, read through config. In a Laravel app with config:cache, getenv() inside a config file is fine; getenv() inside application code is not — it returns false after caching. Keep the env() call in config/swap.php only.

Extension point for the future: if you outgrow Swap's opinionated defaults (custom chain composition, bespoke retry/circuit-breaker behaviour, non-standard caching), the same providers live in the lower-level florianv/exchanger library. Designing your Fx wrapper as a thin interface now makes that migration a one-class change later.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo