mrzard/open-exchange-rates-service
A Laravel integration for the Open Exchange Rates API that lets you fetch live and historical currency exchange rates, convert amounts between currencies, and list supported currencies through a simple, configurable service wrapper.
This is a thin Laravel wrapper around the mrzard/open-exchange-rates-php client — it does one job: bind an OpenExchangeRates API client into the container and expose config for your app_id. Given the last release was December 2017 (Laravel 5.5 era) and it has essentially no adoption, treat the first 30 minutes as a compatibility audit rather than a normal install.
1. Install and inspect what you actually got
composer require mrzard/open-exchange-rates-service
Before writing any code, open vendor/mrzard/open-exchange-rates-service/ and read three things:
composer.json → require (which Laravel/Illuminate versions, which PHP) and extra.laravel (does it support package auto-discovery?)src/ → the service provider class name and the container binding keyconfig/ → the config file name and key namesIf extra.laravel.providers is missing, you must register manually in config/app.php:
'providers' => [
// ...
Mrzard\OpenExchangeRates\ServiceProvider::class, // verify exact FQCN
],
2. Publish config and set your key
php artisan vendor:publish --provider="Mrzard\OpenExchangeRates\ServiceProvider"
// config/open-exchange-rates.php (name per the package's config file)
return [
'app_id' => env('OXR_APP_ID'),
// 'base_currency' => 'USD', // if present
];
OXR_APP_ID=your_app_id_here
Get a key from openexchangerates.org — the free tier is the primary use case here.
3. First use case: a currency conversion
Inject the client rather than reaching for a facade (see Gotchas on facade aliases):
use Mrzard\OpenExchangeRates\...; // confirm the concrete class from src/
public function __construct(private OpenExchangeRates $oxr) {}
public function priceInEur(float $usd): float
{
$rates = $this->oxr->latest(); // or ->getLatest(), check src/
return $usd * $rates['rates']['EUR'];
}
That round trip — resolve from container, fetch latest rates, read from rates — is 90% of what this package is for.
Constructor injection everywhere. The package's only real value is that the client is a singleton in the container with your key already wired. Never new the client yourself; you lose the config and the ability to fake it.
Wrap it in your own service — don't call the client from controllers. Because the package is unmaintained, you want a single file to change when you inevitably replace it:
final class ExchangeRateService
{
public function __construct(private OpenExchangeRates $client, private int $ttl = 3600) {}
public function rate(string $from, string $to): float
{
$rates = Cache::remember('oxr:latest', $this->ttl, fn () => $this->client->latest());
return $rates['rates'][$to] / $rates['rates'][$from];
}
}
Cache aggressively — the client does not. This is the most important pattern in the whole package. Free-tier quota is small (historically ~1,000 requests/month), and latest() on a page load will burn it in days. Cache the full rates array, not individual pairs, and use a TTL matched to the provider's refresh cadence (hourly for free tier).
Schedule a warm-up instead of cache-miss fetches. A scheduled job that writes oxr:latest every hour means user requests never touch the API:
// app/Console/Kernel.php
$schedule->call(fn () => Cache::put('oxr:latest', app(OpenExchangeRates::class)->latest(), 7200))
->hourly()
->withoutOverlapping();
Then your read path is Cache::get('oxr:latest') — fast, quota-safe, and degrades gracefully if the API is down.
Test by swapping the binding. $this->app->instance(OpenExchangeRates::class, $fake) (or $this->mock(...)) in a test case. For a thin API wrapper this is often easier than fighting the package's own test setup, which likely targets Laravel 5.4-era PHPUnit.
Historical/time-series endpoints. If src/ exposes historical($date) / timeSeries(...), note these hit different endpoints with different quota costs — route them through a separate cache namespace and don't schedule them on the same tight cadence as latest().
Compatibility is the first bug you'll hit. A 2017-12 release was built against Illuminate 5.x. On Laravel 9/10/11 the provider may fail to boot over removed helpers or bind differences. Check composer.json constraints before composer require and be ready to either pin a compatible Laravel or fork the provider. If you only need the client, installing mrzard/open-exchange-rates-php directly and binding it yourself in AppServiceProvider is often less work than making the wrapper boot.
Config quirks are silent. If app_id is empty, most versions of the underlying client fail at request time with a generic HTTP error rather than at boot. Add a guard: abort_unless(config('open-exchange-rates.app_id'), 'OXR_APP_ID missing') somewhere in a boot/test path.
php artisan config:cache will freeze your key. Standard Laravel trap, but acute here because the wrapper leans entirely on config. Always re-run config:cache after changing OXR_APP_ID, and never call env() directly from your own code — go through the package config.
Facade aliases may not be registered. If extra.laravel.aliases is absent, OpenExchangeRates::latest() throws "class not found" until you add the alias to config/app.php. Constructor injection sidesteps this; prefer it.
Currency codes are uppercase and case-sensitive. API returns USD, EUR, etc. $rates['rates']['eur'] is null, and multiplying by null silently yields 0 in PHP — a conversion bug that looks like a data bug.
Historical rates are UTC. historical('2024-01-01') means midnight UTC. An order timestamped late on Dec 31 local time may need the previous day's rate. Convert explicitly, don't assume server timezone.
Handle API errors yourself. The client will throw on non-200 (invalid key, quota exceeded, rate-limited 429). Wrap calls in try/catch with a fallback — a stale cached rate is almost always better than a 500 on a checkout page:
try {
return $this->fetchFresh();
} catch (\Throwable $e) {
Log::warning('OXR failed', ['e' => $e->getMessage()]);
return Cache::get('oxr:latest') ?? throw $e;
}
Extension points are minimal. Don't expect a manager/driver pattern or a contract to implement. If you need behavior changes (retry, base-currency handling, caching), extend your own wrapper class or fork — patching the provider's register() is a maintenance debt you'll regret.
Plan an exit. Given 1 star, 0 dependents, and a 7+ year gap, budget for replacing this with Http::withToken(...)->get('https://openexchangerates.org/api/latest.json') — about 30 lines. Keeping the dependency confined behind your own service class (above) makes that a one-file change.
How can I help you explore Laravel packages today?