Weave Code
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

PHP 8.2+ currency exchange rate library with a single API over 30+ providers. Supports conversion, historical rates, PSR-16 caching, and provider fallback. Works with PSR-18 HTTP clients and PSR-17 factories for flexible integrations.

View on GitHub
Deep Wiki
Context7

Documentation

This is the technical reference for Swap. For the project overview and ecosystem (Exchanger, Laravel Swap, Symfony Swap), see the README.

Index

📦 Installation

Swap requires PHP 8.2 or newer. It does not bundle an HTTP client; any PSR-18 client paired with a PSR-17 request factory works, and php-http/discovery finds them automatically.

The simplest modern install:

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

Other PSR-18 clients work the same way, for example Guzzle 7:

composer require florianv/swap php-http/guzzle7-adapter nyholm/psr7

You can also pass a client explicitly via Builder::useHttpClient() if you do not want auto-discovery.

⚙ Configuration

Building Swap

Swap is built with the Builder class. A typical setup uses fastFOREX (the project's sponsor) as the primary provider:

use Swap\Builder;

$swap = (new Builder())
    ->add('fastforex', ['api_key' => getenv('FASTFOREX_API_KEY')])
    ->build();

add() registers a provider by its identifier (here fastforex). The full list lives in Provider configuration.

For a no-key starting point, the European Central Bank publishes EUR-base rates for free:

$swap = (new Builder())
    ->add('european_central_bank')
    ->build();

Adding multiple providers

You can chain several providers. Each one is configured with its own options array:

use Swap\Builder;

$swap = (new Builder())
    ->add('fastforex', ['api_key' => getenv('FASTFOREX_API_KEY')])
    ->add('european_central_bank') // free fallback for EUR-base pairs
    ->build();

Identifiers and the configuration keys each one accepts are documented in the Provider configuration section.

How the fallback chain works

Swap calls providers in declaration order. For each provider:

  1. If the provider does not support the requested currency pair, it is skipped silently.
  2. If the provider throws an exception, the exception is collected and the next provider is tried.
  3. If a provider returns a rate, that rate is returned to the caller and the remaining providers are not called.

If every provider was skipped or threw, Swap raises an Exchanger\Exception\ChainException containing all collected exceptions. The chain does not retry the same provider, and there is no built-in delay between attempts.

⚡ Usage

Retrieving rates

Swap exposes two methods, latest() for the most recent rate and historical() for a rate on a given date:

// Latest rate
$rate = $swap->latest('EUR/USD');

echo $rate->getValue();                 // e.g. 1.0823
echo $rate->getDate()->format('Y-m-d'); // e.g. 2026-04-29

// Historical rate
$rate = $swap->historical('EUR/USD', (new \DateTime())->modify('-15 days'));

Currencies are expressed as their ISO 4217 code.

Both methods accept an options array as a third argument; see Per-query options for the supported keys.

Inspecting the rate

The returned Exchanger\Contract\ExchangeRate exposes:

$rate->getValue();         // float
$rate->getDate();          // DateTimeInterface
$rate->getCurrencyPair();  // Exchanger\CurrencyPair
$rate->getProviderName();  // string, the identifier that returned the rate

getProviderName() is useful when several providers are chained: the returned value is the identifier of the provider that actually answered, for example european_central_bank.

Converting amounts

Swap returns rates, not amounts. Conversion stays explicit in your application:

$rate = $swap->latest('EUR/USD');

$amountEur = 100.00;
$amountUsd = $amountEur * $rate->getValue();

For financial code with proper rounding semantics, pair Swap with moneyphp/money. Money ships a first-class SwapExchange that wraps any Swap\SwapInterface directly:

use Money\Money;
use Money\Currency;
use Money\Converter;
use Money\Currencies\ISOCurrencies;
use Money\Exchange\SwapExchange;

$exchange  = new SwapExchange($swap);
$converter = new Converter(new ISOCurrencies(), $exchange);

$eur100 = Money::EUR(100);                              // €1.00 (100 minor units)
$usd    = $converter->convert($eur100, new Currency('USD'));

// Money 4.x also exposes the rate pair alongside the converted amount:
[$usd, $pair] = $converter->convertAndReturnWithCurrencyPair($eur100, new Currency('USD'));

💾 Caching

PSR-16 SimpleCache (minimal setup)

Swap caches results through a PSR-16 SimpleCache. Any PSR-16 implementation works. A minimal Symfony Cache example:

composer require symfony/cache
use Swap\Builder;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;

$cache = new Psr16Cache(new FilesystemAdapter());

$swap = (new Builder(['cache_ttl' => 3600, 'cache_key_prefix' => 'myapp-']))
    ->useSimpleCache($cache)
    ->add('fastforex', ['api_key' => getenv('FASTFOREX_API_KEY')])
    ->build();

All rates returned by Swap are now cached for 3600 seconds, keyed with the prefix myapp-.

If only PSR-6 adapters are available, you can bridge them to PSR-16 with cache/simple-cache-bridge. For example with Predis:

composer require cache/predis-adapter cache/simple-cache-bridge
use Cache\Adapter\Predis\PredisCachePool;
use Cache\Bridge\SimpleCache\SimpleCacheBridge;

$client = new \Predis\Client('tcp://127.0.0.1:6379');
$cache  = new SimpleCacheBridge(new PredisCachePool($client));

Per-query options

Cache behavior can be overridden per call by passing an options array to latest() or historical().

Option Type Default Effect
cache_ttl int null Cache TTL in seconds. null means entries do not expire.
cache bool true Set to false to bypass the cache for this call.
cache_key_prefix string "" Prefix for the cache key. Max 24 characters (PSR-6 limits keys to 64 chars; the internal hash takes 40).

PSR-6 does not allow the characters {}()/\@: in keys; Swap replaces them with -.

$rate = $swap->latest('EUR/USD', ['cache' => false]);
$rate = $swap->latest('EUR/USD', ['cache_ttl' => 60]);
$rate = $swap->latest('EUR/USD', ['cache_key_prefix' => 'currencies-special-']);

HTTP request caching

Some providers return all rates for a given base currency in a single response. If you fetch several pairs sharing the same base (for example EUR/USD and then EUR/GBP), caching the underlying HTTP response avoids hitting the provider twice. Decorate your HTTP client with the PHP HTTP cache plugin and pass it to Builder::useHttpClient():

composer require php-http/cache-plugin cache/array-adapter
use Cache\Adapter\PHPArray\ArrayCachePool;
use Http\Adapter\Guzzle7\Client as GuzzleClient;
use Http\Client\Common\Plugin\CachePlugin;
use Http\Client\Common\PluginClient;
use Http\Message\StreamFactory\GuzzleStreamFactory;
use Swap\Builder;

$pool          = new ArrayCachePool();
$streamFactory = new GuzzleStreamFactory();
$cachePlugin   = new CachePlugin($pool, $streamFactory);
$client        = new PluginClient(new GuzzleClient(), [$cachePlugin]);

$swap = (new Builder())
    ->useHttpClient($client)
    ->add('fastforex', ['api_key' => getenv('FASTFOREX_API_KEY')])
    ->build();

$rate = $swap->latest('EUR/USD'); // performs an HTTP request
$rate = $swap->latest('EUR/GBP'); // hits the HTTP cache

🔑 Provider configuration

Commercial providers

Commercial providers require an API key. The option name varies by provider. The project's sponsor fastFOREX (fastforex) is the recommended starting point.

Identifier Required option Optional flags
fastforex api_key
abstract_api api_key
apilayer_currency_data api_key
apilayer_exchange_rates_data api_key
apilayer_fixer api_key
coin_layer access_key paid (bool)
currency_converter access_key enterprise (bool)
currency_data_feed api_key
currency_layer access_key enterprise (bool)
exchange_rates_api access_key
fixer access_key
fixer_apilayer api_key
forge api_key
open_exchange_rates app_id enterprise (bool)
xchangeapi api-key (note the hyphen)
xignite token

Note: cryptonator, exchangeratehost and webservicex are commercial upstream services but the current Exchanger wrapper does not enforce any option for them. They can be added with ->add('exchangeratehost') until the wrapper is updated to require an API key.

Public providers

Public providers (the European Central Bank, the national banks) need no configuration. Add them by identifier:

$swap = (new Builder())
    ->add('european_central_bank')
    ->add('national_bank_of_romania')
    ->build();
Identifier Base Quote Historical
bulgarian_national_bank * BGN Yes
central_bank_of_czech_republic * CZK Yes
central_bank_of_republic_turkey * TRY Yes
central_bank_of_republic_uzbekistan * UZS Yes
european_central_bank EUR * Yes
national_bank_of_georgia * GEL Yes
national_bank_of_romania (limited list) (limited list) Yes
national_bank_of_republic_belarus * BYN Yes
national_bank_of_ukraine * UAH Yes
russian_central_bank * RUB Yes

Example

Chaining fastFOREX as the primary provider with a couple of fallbacks:

$swap = (new Builder())
    ->add('fastforex',           ['api_key' => getenv('FASTFOREX_API_KEY')])
    ->add('apilayer_fixer',      ['api_key' => 'YOUR_KEY'])
    ->add('open_exchange_rates', ['app_id'  => 'YOUR_APP_ID', 'enterprise' => false])
    ->add('european_central_bank') // free fallback for EUR-base pairs
    ->build();

The array provider is a special case used in tests and fixtures. It accepts a nested structure of latest and historical rates:

$swap = (new Builder())
    ->add('array', [
        ['EUR/USD' => 1.1, 'EUR/GBP' => 1.5],          // latest rates
        ['2017-01-01' => ['EUR/USD' => 1.5]],          // historical rates
    ])
    ->build();

🧩 Creating a custom service

You can register your own provider as long as it implements the same contract used internally. If your service makes HTTP requests, extend Exchanger\Service\HttpService; otherwise extend the simpler Exchanger\Service\Service.

Standard service

The example below registers a Constant service that returns a fixed rate value:

use Exchanger\Contract\ExchangeRateQuery;
use Exchanger\Contract\ExchangeRate;
use Exchanger\Service\HttpService;
use Swap\Service\Registry;

class ConstantService extends HttpService
{
    public function getExchangeRate(ExchangeRateQuery $exchangeQuery): ExchangeRate
    {
        // To call an HTTP endpoint:
        // $content = $this->request('https://example.com');

        return $this->createInstantRate($exchangeQuery->getCurrencyPair(), $this->options['value']);
    }

    public function processOptions(array &$options): void
    {
        if (!isset($options['value'])) {
            throw new \InvalidArgumentException('The "value" option must be provided.');
        }
    }

    public function supportQuery(ExchangeRateQuery $exchangeQuery): bool
    {
        // Example: only support EUR-base pairs.
        return 'EUR' === $exchangeQuery->getCurrencyPair()->getBaseCurrency();
    }

    public function getName(): string
    {
        return 'constant';
    }
}

// Register the service so Builder::add() recognizes its identifier
Registry::register('constant', ConstantService::class);

$swap = (new Builder())
    ->add('constant', ['value' => 10])
    ->build();

echo $swap->latest('EUR/USD')->getValue(); // 10

Historical service

To support historical rates, use the SupportsHistoricalQueries trait. Rename getExchangeRate to getLatestExchangeRate (now protected) and implement getHistoricalExchangeRate:

use Exchanger\Contract\ExchangeRateQuery;
use Exchanger\Contract\ExchangeRate;
use Exchanger\HistoricalExchangeRateQuery;
use Exchanger\Service\HttpService;
use Exchanger\Service\SupportsHistoricalQueries;

class ConstantService extends HttpService
{
    use SupportsHistoricalQueries;

    protected function getLatestExchangeRate(ExchangeRateQuery $exchangeQuery): ExchangeRate
    {
        return $this->createInstantRate($exchangeQuery->getCurrencyPair(), $this->options['value']);
    }

    protected function getHistoricalExchangeRate(HistoricalExchangeRateQuery $exchangeQuery): ExchangeRate
    {
        return $this->createInstantRate($exchangeQuery->getCurrencyPair(), $this->options['value']);
    }
}

❓ FAQ

What happens when every provider fails?

Swap throws an Exchanger\Exception\ChainException. Calling $exception->getExceptions() on it returns the list of exceptions collected from each provider in the chain.

Can I use Swap without an API key?

Yes. The European Central Bank and the national banks listed under Public providers require no key. A few commercial providers (cryptonator, exchangeratehost, webservicex) can also currently be used without one, since the Exchanger wrapper does not yet enforce an option for them.

How does Swap relate to Exchanger?

Swap is the high-level, easy-to-use API. Exchanger is the lower-level provider layer Swap is built on. Reach for Exchanger directly only when you need finer control over chain composition, caching, or HTTP plumbing. See the README's Which package should I use? section.

How do I cache rates?

Configure any PSR-16 cache via Builder::useSimpleCache(). See PSR-16 SimpleCache (minimal setup).

How do I disable cache for a single query?

Pass ['cache' => false] as the options argument: $swap->latest('EUR/USD', ['cache' => false]).

How do I add my own provider?

Implement Exchanger\Contract\ExchangeRateService (or extend HttpService / Service), register it with Swap\Service\Registry::register(), then call Builder::add() with your identifier. See Creating a custom service.

Where is the full provider list with capabilities?

In the Provider configuration section above, split into Commercial and Public tables with identifier, base currency, quote currency and historical support.

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.
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope