overtrue/laravel-revaluation
A Laravel package for currency revaluation, offering configurable exchange rates and simple helpers to convert and update monetary values, helping keep prices, invoices, and other currency-based data consistent across your application.
Install and wire it up
composer require overtrue/laravel-revaluation
On Laravel 5.5+ package discovery registers the service provider automatically. On older apps add it manually to config/app.php:
'providers' => [
Overtrue\LaravelRevaluation\RevaluationServiceProvider::class,
],
'aliases' => [
'Revaluation' => Overtrue\LaravelRevaluation\Revaluation::class,
],
Publish the config and put every credential in .env:
php artisan vendor:publish --provider="Overtrue\LaravelRevaluation\RevaluationServiceProvider"
Where to look first (in this order)
config/revaluation.php — this is the documentation. It tells you the shape of currencies, the default/base currency, the selected rate driver, cache TTL, and the provider credentials key.RevaluationServiceProvider — shows what is bound as a singleton, whether the manager is a driver manager (extendable) or a single class, and which config keys it reads.convert, rate, revalue, refresh), so read the file end to end rather than guessing.Naming has drifted between releases and the package has no real docs, so treat the class names above as a map, not gospel — a five-minute grep -r "public function" will pin down the exact signatures.
First use case: convert an invoice line
$total = Revaluation::convert(12500, 'USD', 'EUR'); // minor units in, minor units out
Confirm the return type (float vs. int vs. a value object) before writing anything else — the rest of your codebase will be built on that assumption.
Convert at the edges, store one canonical currency
Keep your database in a single base currency (integer minor units) and convert only when rendering or when a foreign-currency document must be issued. Storing converted values permanently creates a second source of truth that silently rots as rates move.
class PricePresenter
{
public function __construct(private Revaluation $revaluation) {}
public function forCurrency(int $amountMinor, string $currency): int
{
return $this->revaluation->convert($amountMinor, config('app.currency'), $currency);
}
}
Prefer constructor injection over the facade in services — the facade is convenient in controllers and Tinker, but it makes the rate source untestable.
Snapshot the rate, not just the result
For invoices, orders, and refunds, persist the rate and its timestamp alongside the amount. Revaluation is not idempotent: re-running a conversion a week later produces a different number, and auditors will ask which rate was applied.
$rate = Revaluation::rate('USD', 'EUR');
Invoice::create([
'amount' => $amountMinor,
'currency' => 'EUR',
'fx_rate' => $rate,
'fx_rate_fetched_at'=> now(),
'fx_source' => config('revaluation.driver'),
]);
Cache deliberately and warm it on a schedule
The package-level cache exists to stop you hammering a third-party API. Two habits make it reliable:
// app/Console/Kernel.php
$schedule->call(fn () => Revaluation::refresh())->hourly()->withoutOverlapping();
For batch jobs (monthly reporting over 50k rows), fetch the rate table once into an array and do arithmetic in PHP — calling convert() per row turns a local loop into thousands of cache reads.
Write your own rate provider
This is the package's main extension point and the reason to tolerate its age. Implement the provider contract against your source (ECB XML, an internal treasury service, a rate table in your own DB) and register it in config. For anything but a toy, an internal provider backed by a table you control is the right choice — it removes the external dependency from your request path entirely.
class TreasuryRateProvider implements RateProviderInterface
{
public function rate(string $from, string $to): float
{
return TreasuryRate::query()
->where('base', $from)->where('quote', $to)
->latest('effective_at')->value('rate')
?? throw new RateUnavailableException("$from/$to");
}
}
Test with a fixed-rate fake
Bind a deterministic provider in the test container so no test touches the network and every assertion has an exact expected value.
$this->app->instance(RateProviderInterface::class, new FixedRateProvider(['USD/EUR' => 0.92]));
$this->assertSame(9200, Revaluation::convert(10000, 'USD', 'EUR'));
It is archived (last release March 2018). Treat it as vendored code you own. It targets the Laravel 5.x era; expect friction on Laravel 9+/PHP 8 (return types, Str/Arr helpers, container signatures). If you only need conversion arithmetic, a 40-line service class plus your own rate table is often the better trade — the package's value is the provider abstraction and caching, not the maths.
Never do money maths in floats. The underlying conversion almost certainly multiplies floats. Round only at the display boundary and use bcmath/integer minor units for anything that is summed, refunded, or reconciled.
Mind the direction of the rate. "Rate for USD→EUR" can mean 1 USD = 0.92 EUR or 1 EUR = 1.087 USD depending on the provider. One wrong assumption here is a 100× error that only shows up in production. Assert against a known pair in a test.
Missing pairs may return 0 rather than throwing. Check the failure behaviour explicitly — a silent zero becomes a free product. Wrap conversions in a guard that rejects non-positive rates.
Handle provider outages. Add a try/catch that falls back to the last known rate (with its age recorded) or fails the transaction loudly. A checkout that 500s because an exchange API is down is worse than a slightly stale rate.
php artisan config:cache will bite you. Anything read via env() inside a package config file returns null once config is cached. Always reference credentials through config('revaluation.api_key') and ensure the env var is set at deploy time.
Cache keys must include the pair (and possibly the driver). If you switch drivers or add a currency, stale entries can serve rates from the wrong source. Flush the relevant keys in your provider's deploy/refresh routine.
Zero-decimal currencies are a real trap (JPY, KRW). If the config's currency definitions carry a precision/decimals field, use it for formatting; if not, keep your own map — otherwise you will render ¥1000.00.
Debugging: php artisan tinker then dump the manager instance and the resolved config to see which driver is actually active; log the provider HTTP calls at debug level; and check storage/logs for swallowed exceptions in the cache layer. If a conversion looks wrong, log the raw rate before any rounding.
Extend, don't fork. Use the driver manager's extend() (or a container binding for the provider interface) to register custom behaviour. A small wrapper service in your own app/ namespace that owns the package behind an interface means you can swap it out in a day if it finally breaks — which, given the archive date, it eventually will.
How can I help you explore Laravel packages today?