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

Exchange Laravel Package

worksome/exchange

Currency exchange for Laravel: convert money between currencies through a clean, driver-based API. Swap exchange-rate providers, cache rates, and keep conversion logic testable and framework-friendly.

View on GitHub
Deep Wiki
Context7

Getting Started

worksome/exchange is a Laravel wrapper around currency-rate APIs (Frankfurter, ExchangeRate.host, Fixer, CurrencyAPI, etc.) that gives you a single Exchange facade, a normalised Rates value object, and framework-native caching.

Minimum setup:

  1. composer require worksome/exchange
  2. Publish the config: php artisan vendor:publish --tag="exchange-config" — this gives you config/exchange.php with driver, cache, and per-driver drivers.* blocks.
  3. Set the driver and credentials in .env (e.g. EXCHANGE_DRIVER=frankfurter, plus any API key the driver needs). Frankfurter needs no key; Fixer/CurrencyAPI/OpenExchangeRates do.
  4. Read, in this order: config/exchange.php (what's actually enabled), the Worksome\Exchange\Facades\Exchange facade, and Worksome\Exchange\Rates. Those three files are the whole public surface you'll touch day to day.

First real use case — price a basket in the user's currency:

use Worksome\Exchange\Facades\Exchange;

$rates = Exchange::getRates('GBP');       // Rates DTO for a base currency

$rates->baseCurrency;                     // 'GBP'  (string)
$rates->rates;                            // ['USD' => 1.27, 'EUR' => 1.17, ...]
$rates->retrievedAt;                      // Carbon instance

Upgrading from v2.x — do this before anything else:

  • Replace every $rates->getBaseCurrency(), $rates->getRates(), $rates->getRetrievedAt() call with the public properties baseCurrency, rates, retrievedAt. These getters are gone in v3.0.0, not deprecated. grep -rn "getBaseCurrency\|getRates()\|getRetrievedAt" app/ tests/ first.
  • If (and only if) you self-host Frankfurter, update FRANKFURTER_BASE_URL from the /v1 to the /v2 endpoint — otherwise every call throws.
  • If you were relying on the old Frankfurter behaviour and want the v2 provider explicitly, v3 also ships a frankfurter_v2 driver you can select.

Implementation Patterns

Wrap the facade in your own service. Controllers should never call Exchange directly; you want one seam to fake, cache, and add fallbacks to.

final class CurrencyConverter
{
    public function convert(int $amountInMinorUnits, string $from, string $to): int
    {
        $rates = Exchange::getRates($from);

        return (int) round($amountInMinorUnits * $rates->rates[$to]);
    }
}

Persist rates, don't fetch per request. The DTO is cheap but the HTTP call is not. Either rely on the package cache config (cache.store, TTL) or store a Rates payload in your own table/Redis key and hydrate on boot. Cache key by base currency, since that's the granularity the API gives you.

Stamp your UI with retrievedAt. Because the property is a real Carbon, it's trivial to render "rates as of …" or to refuse to quote if retrievedAt->diffInHours() > 12. This is the single most useful thing the DTO gives you in production.

Refresh out of band. A scheduled command that pulls a handful of base currencies and writes them to your store beats lazy fetching — it keeps request latency flat and turns API outages into a stale-rate warning rather than a 500.

// app/Console/Commands/RefreshRates.php
foreach (config('shop.base_currencies') as $base) {
    Cache::put("rates:{$base}", Exchange::getRates($base), now()->addHour());
}

Multi-currency display. Store amounts in a single base currency (integer minor units) and convert at read time. Never store converted values alongside the base — you'll be re-deriving them the moment rates move.

Testing. Swap the driver to a test/stub driver in phpunit.xml, or Http::fake() the underlying driver endpoint and assert on the parsed Rates object. Because Rates is now property-only, assertions become simple array/equality checks:

Http::fake([/* driver endpoint */ => Http::response([...])]);

$rates = Exchange::getRates('GBP');
expect($rates->rates)->toHaveKey('USD')
    ->and($rates->baseCurrency)->toBe('GBP');

Custom drivers. The manager resolves drivers from the drivers array in config; registering a bespoke provider there is the supported extension point when you have an internal FX service or a vendor the package doesn't cover.

Gotchas and Tips

  • The getter removal is silent at the language level but fatal at runtime. $rates->getRates() on v3 throws BadMethodCallException/Error depending on how it's reached — and because rates is a public property, a typo'd $rates->rate returns null with no warning. Add a static analysis pass (phpstan/larastan) after the upgrade; property access is much harder to catch by grep than a method call.
  • baseCurrency is now cast to float (per the v3 changelog, "ensure base currency is float"). If you'd been relying on an int or string there — === comparisons, strtoupper() calls, JSON shape — check those call sites. Loosen strict comparisons around it.
  • Frankfurter v2 is a hard break for self-hosters. Bumping the container but forgetting FRANKFURTER_BASE_URL=/v2 produces 404s that look like auth/network errors. Confirm the endpoint in config/exchange.php and in your deployed .env — env values beat config defaults, and .env files are the usual place this gets missed.
  • The new frankfurter_v2 driver and the updated frankfurter driver overlap. Pick one deliberately; having both configured with different base URLs is a great way to get inconsistent rates across queue workers.
  • Floats are for display, not arithmetic. $rates->rates gives you floats. Convert into brick/money, Money\Money, or integer minor units before you multiply, sum, or persist. Rounding drift compounds fast on order totals and invoices.
  • Base currency rarely appears in rates. Don't assume $rates->rates['GBP'] exists when the base is GBP — short-circuit if ($from === $to) return $amount; before indexing. Missing keys are a null/undefined-index warning away from a wrong price.
  • Cache is keyed by driver-agnostic inputs. Switching EXCHANGE_DRIVER or changing a base URL does not invalidate previously cached rates. Flush the relevant cache keys on deploy whenever you change driver, base URL, or upgrade a major version.
  • Quota and rate-limit errors surface as exceptions, not empty results. Wrap calls in a try/catch with a stale-cache fallback, and keep an eye on free-tier limits (Fixer and CurrencyAPI in particular are request-capped). Frankfurter is free but also the most commonly rate-limited in bulk-refresh loops — batch and back off.
  • retrievedAt is your debugging lever. When a number looks wrong, dump baseCurrency, retrievedAt, and the relevant key before you suspect the maths; nine times out of ten it's a stale cache entry or a base-currency mismatch.
  • Not all drivers support all base currencies. Some pin you to EUR or USD. If you offer an exotic base, verify driver support before the feature ships — otherwise the failure appears in production for one currency only.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr