Commands, events & scheduling
| Command | Purpose |
|---|---|
money:currencies {--iso} {--custom} {--code=*} | List the registry: code, numeric code, exponent, symbol, name and origin. |
money:rates:refresh {source=ecb} {--feed=daily} {--from=} {--to=} {--queue} | Fetch rates into the table. --feed is daily, recent or history; --queue dispatches a unique job; supports --isolated. |
money:rates:prune {--before=} {--keep-days=} {--include-manual} {--pretend} | Delete old rates. One bound is required; manual rows stay unless --include-manual; --pretend only counts. |
money:convert {amount} {from} {to} {--date=} {--driver=} | Convert an amount and print the rate, source, date and result. |
php artisan money:currencies --custom
php artisan money:rates:refresh # ECB daily feed into the rates table
php artisan money:rates:refresh ecb --feed=recent # the last 90 days
php artisan money:rates:refresh ecb --feed=history --from=2020-01-01 --to=2020-12-31
php artisan money:rates:refresh --queue # dispatch a unique queued job instead
php artisan money:rates:refresh --isolated # keep overlapping runs out
php artisan money:rates:prune --keep-days=365 --pretend # only count
php artisan money:rates:prune --before=2024-01-01 --include-manual
php artisan money:convert 19.99 EUR USD
php artisan money:convert 19.99 EUR USD --date=2026-09-01 --driver=databaseScheduled refresh
With MONEY_EXCHANGE_SCHEDULE=true the provider schedules money:rates:refresh on exchange.refresh.cron — 30 16 * * 1-5 in Europe/Berlin, just after the ECB publishes around 16:00 CET — with withoutOverlapping() and onOneServer(). Your app’s scheduler must be running. It’s off by default.
Queued refresh
use RoundlyConsulting\Money\Enums\EcbFeed;
use RoundlyConsulting\Money\Facades\Exchange;
use RoundlyConsulting\Money\Jobs\RefreshExchangeRates;
Exchange::rates()->refreshLater(feed: EcbFeed::Recent); // queue the refresh
// the same job, dispatched yourself: source, feed, ?from, ?to (Y-m-d)
dispatch(new RefreshExchangeRates('ecb', 'recent'));refreshLater() dispatches the RefreshExchangeRates job, which is unique per source and feed for ten minutes and holds scalars only.
Refreshing and pruning from code
use RoundlyConsulting\Money\Enums\EcbFeed;
use RoundlyConsulting\Money\Facades\Exchange;
$result = Exchange::rates()->refresh('ecb', EcbFeed::Recent); // RefreshResult
$result->stored; // rows written
$result->skippedManual; // manual rows left untouched
$result->skippedUnknownCurrency; // feed currencies the registry does not know
$result->latestDate; // ?CarbonImmutable
$deleted = Exchange::rates()->prune(now()->subYear()); // intA refresh defaults to the last 7 days (daily feed), 90 days (recent) or everything since 1999 (history). A failed refresh never deletes rows. Prune returns the number of rows deleted — or counted, when pretending. The actions behind these calls are under DI and actions.
Events
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Money\Events\ExchangeRatesRefreshed;
use RoundlyConsulting\Money\Events\ExchangeRatesRefreshFailed;
Event::listen(function (ExchangeRatesRefreshed $event) {
logger()->info('Exchange rates refreshed', [
'source' => $event->result->source,
'stored' => $event->result->stored,
'latest' => $event->result->latestDate?->toDateString(),
]);
});
Event::listen(function (ExchangeRatesRefreshFailed $event) {
// alert your team: $event->source, $event->message — existing rows are untouched
});- ExchangeRatesRefreshed — RefreshResult $result: source, stored, skippedManual, skippedUnknownCurrency, latestDate.
- ExchangeRatesRefreshFailed — string $source and string $message, fired before the exception is rethrown. Hook your alerting here.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.