qbil-software/openexchangeratesbundle
Symfony bundle for integrating the Open Exchange Rates API, providing configurable services to fetch currency exchange rates and use them in PHP applications. Simplifies API access and rate handling.
This package is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no config/ file that Laravel auto-merges, and no facade. Before installing, understand that you will be using it as a vendor library, not as an out-of-the-box Laravel integration.
Minimal steps:
composer require qbil-software/openexchangeratesbundle
Because it targets Symfony 2.8+ (~2019), expect it to pull an old openexchangerates/Guzzle client. Check composer show after install to see the actual underlying client version.vendor/qbil-software/openexchangeratesbundle/ — specifically the service classes and the bundle's Resources/config (services XML/YAML). That is where the real client is instantiated and where the API key/base URL are wired. Ignore QbilSoftwareOpenExchangeRatesBundle.php and the DependencyInjection/ folder; those are Symfony-only.registerBundles() does not exist. Bind the underlying client manually in AppServiceProvider.First realistic use case: converting stored prices into a display currency (e.g. showing a USD-priced SaaS product in EUR), or normalizing order totals to a reporting currency. Start with a single rates()->latest()-style call in a tinker session using a hand-constructed client, cache the result, and only then wrap it in application code.
Bind the underlying client as a singleton. Do not try to use the Symfony service container. Locate the concrete client class in the bundle and register it:
// AppServiceProvider::register()
$this->app->singleton(Client::class, function () {
return new Client(config('services.openexchangerates.app_id'));
});
If the bundle only exposes Symfony Definitions, copy the instantiation logic from Resources/config/services.* into your provider — it is usually just new Client($appId).
Wrap it in a thin application service. Keep the vendor class out of controllers:
class ExchangeRates
{
public function __construct(private Client $client, private Cache $cache) {}
public function rate(string $from, string $to): float
{
$rates = $this->cache->remember('oer.rates', now()->addHours(6), fn () =>
$this->client->getLatest()->getRates()
);
// Free tier is USD-based: cross-rate through USD.
return ($rates[$to] ?? 1) / ($rates[$from] ?? 1);
}
}
Cache aggressively and refresh on a schedule. The API is rate-limited and the free plan only allows monthly/hourly updates depending on tier. A scheduled command that writes rates to a currency_rates table is far better than hitting the API per request:
// app/Console/Commands/SyncRates.php
$rates = $this->client->getLatest()->getRates();
foreach ($rates as $code => $rate) {
CurrencyRate::updateOrCreate(['code' => $code], ['rate' => $rate, 'fetched_at' => now()]);
}
Register it in routes/console.php or the kernel with ->hourly().
Read rates from your own table in hot paths. Never make a network call inside a Blade view or an API resource. Inject a CurrencyRepository that reads the cached table and falls back to the last-known value if a currency is missing.
Format with a dedicated money library. Pair the rate lookup with moneyphp/money (or brick/money) so rounding and currency metadata stay correct rather than doing round($amount * $rate, 2) by hand.
Testing pattern. Bind a fake client in tests:
$this->mock(Client::class, fn ($m) => $m->shouldReceive('getLatest')->andReturn($fakeResponse));
Or better, don't mock the vendor type — mock your own ExchangeRates interface so vendor churn never breaks tests.
extra.laravel.providers will be empty. There is no publishes(), no config() helper key, and no artisan command shipped with it. Everything above is manual wiring — factor that into your estimation.composer require conflicts with your Laravel version, you're better off using the underlying openexchangerates PHP client directly, or calling the REST endpoint with Laravel's Http facade:
Http::get("https://openexchangerates.org/api/latest.json", ['app_id' => $key])->json('rates');
This is often the pragmatic "day-to-day" answer — the bundle adds little value inside Laravel.base=EUR on the free plan; always cross-rate through USD. Watch for double rounding when chaining conversions.429/503 or fall back to your cached table). A missing/expired key typically returns a 401 body with a description field rather than throwing on its own.Configuration.php file defines the exact config keys (often a parameter prefixed qbil_open_exchange_rates.*). Read it to know whether the API key is a constructor argument or a setAppId() call — the answer varies by underlying client version.fetched_at and refuse to compute prices from rates older than a threshold.now()->utc(), not local time, when deciding whether to refresh.exchangerate.host / Frankfurter for free EUR-base rates) rather than retrofitting this bundle.How can I help you explore Laravel packages today?