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

Laravel Currency Laravel Package

amrshawky/laravel-currency

amrshawky/laravel-currency is a Laravel package for currency conversion, formatting, and exchange rates. It provides a simple API, configurable exchange-rate providers, caching, and locale-aware formatting to help build multi-currency applications quickly.

Deep Wiki
Context7

Getting Started

composer require amrshawky/laravel-currency
php artisan vendor:publish --tag=config   # publishes config/currency.php

Open config/currency.php first — it is the only place to set the API key, base URL, and HTTP timeout. Add the key to .env (the key is created on exchangerate.host; the free tier now requires one, so the "no config" examples you'll find in blog posts from 2021 will fail with 401/403):

EXCHANGE_RATES_API_KEY=xxxxxxxxxxxxxxxx

Confirm the exact env var name in the published config — the package has shipped under both EXCHANGE_RATES_API_KEY and CURRENCY_API_KEY naming across versions.

The first use case is almost always a one-off conversion. The API is a fluent builder, every chain ends in ->get():

use AmrShawky\LaravelCurrency\Facade\Currency;

$eur = Currency::convert()
    ->from('USD')
    ->to('EUR')
    ->amount(100)
    ->get();   // float, e.g. 92.41

That single call is enough to validate credentials, network egress from your app host, and that the base URL in config still resolves. Do it from php artisan tinker before wiring anything into a controller.

Implementation Patterns

Fluent chains, one call per concern. The builder is stateless per chain, so compose freely:

// Latest snapshot, multiple targets in one request
Currency::rates()
    ->latest()
    ->base('USD')
    ->symbols(['EUR', 'GBP', 'JOD'])
    ->get();   // ['EUR' => 0.92, 'GBP' => 0.79, ...]

// Historical (single date)
Currency::rates()
    ->historical('2023-01-15')
    ->base('USD')
    ->symbols(['EUR'])
    ->get();

// Time series — use for charts/sparklines
Currency::timeSeries()
    ->dateFrom('2023-01-01')
    ->dateTo('2023-01-31')
    ->base('USD')
    ->symbols(['EUR'])
    ->get();

// Fluctuation — start/end/change per symbol
Currency::fluctuation()
    ->dateFrom('2023-01-01')
    ->dateTo('2023-01-31')
    ->base('USD')
    ->symbols(['EUR'])
    ->get();

// Crypto uses the same convert endpoint
Currency::convert()->from('BTC')->to('USD')->amount(1)->get();

Wrap it in a service, not in controllers. The package has no caching, no retry, and no test double. Put one class between the app and the facade so you can swap it later (the package is archived; see gotchas):

class ExchangeRates
{
    public function convert(float $amount, string $from, string $to): float
    {
        $key = "fx:{$from}:{$to}:" . now()->format('Y-m-d-H');

        return Cache::remember($key, now()->addHour(), fn () =>
            Currency::convert()->from($from)->to($to)->amount($amount)->get()
        );
    }
}

Cache aggressively. Rates change daily at best; hitting the API per request will burn your quota and add 200–800 ms to every page. Cache the rate, not the converted amount, so amount changes stay cheap:

$rates = Cache::remember("fx:USD", 3600, fn () =>
    Currency::rates()->latest()->base('USD')->symbols(['EUR','GBP'])->get()
);
$price = $amount * $rates['EUR'];

Store the rate used on the transaction. For invoices, orders, or anything financial, persist amount, converted_amount, rate, base, target, and fetched_at. You cannot reconstruct which rate was applied later, and historical endpoints are the wrong tool for auditing past business transactions.

Prefer symbols([...]) over N calls. One request with an array is cheaper and returns a single consistent timestamp. Loop only when you need per-pair bases.

Handle failures at the boundary. Wrap the call once in the service class and convert transport/API failures into a domain exception your controller understands:

try {
    $rate = Currency::convert()->from($from)->to($to)->amount(1)->get();
} catch (\Throwable $e) {
    report($e);
    throw new ExchangeRateUnavailable($from, $to, previous: $e);
}

Testing. In tests, mock the facade (Currency::shouldReceive('convert->from->to->amount->get')->andReturn(...)) is painful because of the intermediate builders. It is far easier to fake your own ExchangeRates interface in the container and let the package only ever be exercised in an integration test guard-tagged @group external.

Gotchas and Tips

  • Archived package, moving API. Last release was 2023-02-17. exchangerate.host changed hands and moved to a paid/apilayer-backed model requiring an access_key; the URL has moved from api.exchangerate.host to api.exchangerate.host with auth (and at times api.apilayer.com/exchangerate_data). If calls 401/403, check config/currency.php's base_url against the provider's current docs. Overriding base_url in config (or via .env) is the intended escape hatch and doesn't require forking.
  • Budget for a migration. Treat this as a thin HTTP wrapper you may need to replace. Keep every call behind your own service class so switching to brick/money, moneyphp/money, the ECB feed, or direct Guzzle calls is a one-file change.
  • No built-in caching or retry. Every ->get() is a live HTTP call. Add Cache::remember and, for resilience, a retry in a wrapper (retry(3, fn () => ..., 200) over your service method). The package's own timeout is a config value — set it low (3–5 s) so a slow API doesn't stall web requests.
  • Errors are generic. Failures surface as Guzzle/HTTP exceptions or null/empty arrays depending on the response shape, not as a CurrencyException you can rely on. Always null-check the result before doing arithmetic; null * 100 silently becomes 0.0.
  • Rate limiting is real. Free tiers are quota-limited per month. Time-series and fluctuation calls cost the same as a single latest call but return much more data — cache them for hours, not seconds.
  • Date handling. Historical/time-series dates expect Y-m-d strings; passing a Carbon cast with a timezone may serialize oddly. Format explicitly ($date->toDateString()). Historical data availability is bounded by the provider — very old dates may return empty rather than error.
  • Crypto is not "real" exchange data. Crypto rates come from the same FX provider and are indicative, not an order-book price. Do not use them for anything that needs execution-grade accuracy.
  • Base currency defaults. If you omit ->base(), you get the provider's default (usually USD) — set it explicitly everywhere to avoid silent drift when the provider changes defaults.
  • Extension point. The service provider binds a singleton for the client; if you need to inspect the raw HTTP response (headers, timestamps, quota headers) you'll have to skip the facade and call the endpoint yourself with Http::withToken(...). For debugging, Http::fake() + Http::assertSent() in an integration test is more informative than dumping the builder.
  • Facade aliasing. If you're not on auto-discovery for some reason, register AmrShawky\LaravelCurrency\CurrencyServiceProvider and alias the Currency facade in config/app.php manually; otherwise you'll get "Class Currency not found" in tinker.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route