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

florianv/swap-bundle

Symfony bundle for the Swap currency exchange library. It integrates exchange-rate providers into Symfony, enabling currency conversion, rate lookup, and configurable provider and caching setup through a simple service-oriented API.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the mismatch that shapes everything: florianv/swap-bundle is a Symfony bundle. Laravel has no auto-discovery for it, no swap: config tree wiring, and no Swap\Swap service in the container. What you actually install in a Laravel app is the library it wraps:

composer require florianv/swap

Heads-up on the 6.0.0 release: the version bump that just shipped is for florianv/swap-bundle (symfony-swap), not florianv/swap. Its changelog is entirely Symfony-side: PHP ^8.2 required, Symfony ^6.4 | ^7.0 | ^8.0, php-http/guzzle6-adapter swapped for guzzle7-adapter, FlorianvSwapExtension::load() signature modernised, PHPUnit 11 / Psalm level 1 internals. None of that reaches a Laravel app, because you never install the bundle. The one practical consequence: the bundle is no longer readable on a PHP 8.1 box, so if you keep it in a scratch checkout purely as reference, that checkout now needs PHP 8.2+. The core florianv/swap library is unchanged by this release — don't let the 6.0.0 headline push you into an unnecessary major bump of the library, which has its own independent version line.

The bundle is still useful as documentation: its DependencyInjection/Configuration.php and service definitions show exactly which constructor arguments each provider expects (access keys, base currency, HTTP client, cache) — mirror that in your own provider. Post-6.0 the Configuration.php is simpler (the Symfony <4.2 compatibility branches were deleted), which actually makes it a cleaner reference. If you ever inherit a Symfony app using it, the bundle exposes Swap\Swap as an autowired service and an ordered swap.providers list; that's the mental model to copy.

Where to look first, in this order:

  1. vendor/florianv/swap/README.md — provider list and Swap\Builder API.
  2. vendor/florianv/swap/src/Builder.php — the real contract; it changes between majors.
  3. vendor/florianv/swap/src/Provider/ — one class per data source, tells you the exact options array.
  4. vendor/florianv/swap/src/ExchangeRate.php — what you actually get back.

Minimal wiring — config/swap.php:

return [
    'default' => env('SWAP_PROVIDER', 'fixer'),
    'providers' => [
        'fixer' => ['access_key' => env('FIXER_KEY')],
        'exchange_rates_api' => ['access_key' => env('EXCHANGE_RATES_API_KEY')],
        'european_central_bank' => [], // no key, EUR base only, no weekend rates
    ],
    'cache_ttl' => 3600,
];

Then bind it once in AppServiceProvider::register():

$this->app->singleton(\Swap\Swap::class, function ($app) {
    return (new \Swap\Builder())
        ->add('fixer', config('swap.providers.fixer'))
        ->add('european_central_bank')
        ->useCache(new \App\Support\LaravelPsr16Cache($app['cache']->store()))
        ->build();
});

First use case: price display and checkout in a second currency.

$rate = app(\Swap\Swap::class)->latest('EUR/USD');
$usd  = app(\Swap\Swap::class)->convert(new Money(1999, new Currency('EUR')), 'USD');

Implementation Patterns

Wrap the vendor class — do not inject Swap\Swap into controllers. The library is small, the API has churned across majors, and you will want a last-known-rate fallback and an audit trail. One App\Services\CurrencyConverter with convert(Money $m, string $to): Money and rate(string $base, string $quote): float lets you swap the implementation (or fake it) later.

PSR-16 cache bridge. Swap v4 wants Psr\SimpleCache\CacheInterface; Laravel's repository isn't PSR-16. A ~40-line adapter is the whole integration:

final class LaravelPsr16Cache implements \Psr\SimpleCache\CacheInterface
{
    public function __construct(private \Illuminate\Contracts\Cache\Repository $cache) {}

    private function k(string $key): string
    {
        // PSR-16 reserves {}()/\@: — Swap uses "EUR/USD" style keys
        return 'swap:'.str_replace(['/','\\','{','}','(',')','@',':'], '_', $key);
    }

    public function get(string $key, mixed $default = null): mixed
    { return $this->cache->get($this->k($key), $default); }

    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
    {
        $seconds = $ttl instanceof \DateInterval
            ? (new \DateTimeImmutable())->add($ttl)->getTimestamp() - time()
            : (int) ($ttl ?? config('swap.cache_ttl'));
        return $this->cache->put($this->k($key), $value, $seconds);
    }

    // delete, clear, getMultiple, setMultiple, deleteMultiple, has — trivial passthroughs
}

Without key sanitization, strict PSR-16 implementations throw InvalidArgumentException on the first EUR/USD lookup.

Provider chain as your fallback story. Register providers in priority order; the builder tries each until one succeeds. Put a keyed provider first (Fixer, ExchangeRatesApi, CurrencyLayer, OpenExchangeRates) and european_central_bank last — it needs no credentials and survives quota exhaustion, at the cost of EUR-only and no weekend/holiday quotes.

Layer a database fallback below the chain. Catch the package exception hierarchy (catch (\Swap\Exception\Exception $e)) and read the last persisted rate from a exchange_rates table with a fetched_at column. Persist on every successful fetch — this is also your audit log for refunds and accounting.

Warm the cache on a schedule, not on requests. Exchange rates have no business being fetched during a user's checkout:

// routes/console.php
Schedule::command('swap:warm EUR,USD,GBP,CHF')->hourly();

Inside the command, guard against stampedes with Cache::lock('swap:warm', 60)->block(5, fn () => ...) and log the provider name and rate date for each pair.

Store money as minor units plus the rate used. On an order, write total_minor, currency, base_total_minor, rate_used, rate_date, rate_provider. Without rate_used you cannot reproduce an invoice or process a partial refund six weeks later.

Format at the edge only. Keep float/string precision internally, round once at presentation via moneyphp's IntlMoneyFormatter, and expose a Blade component rather than scattering number_format calls.

Testing: bind a fake converter in the container ($this->app->instance(CurrencyConverter::class, new FakeConverter())) — that's far more stable than trying to stub the vendor. For HTTP-level provider tests, note the providers use PSR-18/Guzzle; Http::buildClient() returns a Guzzle client that respects Http::fake(), so you can pass it into the provider options in tests. Verify the option name in the provider constructor first.

Gotchas and Tips

  • Don't composer require florianv/swap-bundle in Laravel. It pulls Symfony DI/HttpKernel packages and will never boot. Only the library belongs in a Laravel app. The 6.0.0 release doubles down on this: it now requires Symfony ^6.4 minimum (dropping 3.x/4.x/5.x entirely) and PHP ^8.2 — it is more Symfony-shaped than ever, not more framework-agnostic.
  • Don't conflate the two version lines. swap-bundle 6.0.0 says nothing about florianv/swap's version. If you see a Dependabot PR bumping the bundle in a Laravel repo, it's a straggler from a copy-pasted recipe — remove the dependency rather than merging it.
  • getValue() may return a string, not a float. Cast explicitly ((float) $rate->getValue()) and don't do arithmetic on the raw return. Precision is the whole point of the type.
  • getDate() is the rate's publication date, not "now". ECB publishes nothing on weekends or TARGET holidays, so latest('EUR/USD')->getDate() on a Sunday is Friday. Never compare it to now() for staleness logic — compare to your own fetched_at.
  • Quota is real and silent. Fixer's free tier is ~100 requests/month; a missing cache or a cache-key bug turns into a hard outage around day three. Log provider hits and alert on a rate fetched more than N times per hour.
  • Cross rates aren't free. Several providers only quote against a single base. If the pair you ask for isn't supported, you get an exception, not triangulation — derive GBP/JPY yourself from GBP/USD and USD/JPY, and do the multiplication in bcmath or with 6+ decimals.
  • Crypto tickers break ISO assumptions. new Currency('BTC') is fine in moneyphp (it doesn't validate against ISOCurrencies unless you ask), but formatting via IntlMoneyFormatter will fail for non-ISO codes — branch on that.
  • Currency codes must be uppercase ISO 4217. A lowercase 'usd' from a form request produces a confusing provider error. Normalize at the boundary.
  • Caching ExchangeRate objects is fine (they're plain value objects), but the cache key is built from base/quote — which is why the slash issue above bites everyone exactly once.
  • Pin the major version of the library. florianv/swap moves slowly and has a single maintainer; treat it as a leaf dependency behind your own interface so a future major bump is a one-file change. (When you do audit it, note the last round of movement in this family was on the bundle side, not the library — so an untouched swap constraint is expected, not neglect.)
  • Extension point: everything interesting is in the Builder — custom provider, custom HTTP client, custom cache. There's no Laravel-level config to extend, so resist the urge to build a package around it; a service class and a config file are the right size. If you port service definitions from the bundle, take the post-6.0 ones: the Guzzle adapter is now php-http/guzzle7-adapter, so don't copy older snippets that reference the v6 adapter.
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.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules