NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Money for Laravel

Configuration

The package works with zero configuration. The published config/money.php in full:

return [
    'default_currency' => env('MONEY_DEFAULT_CURRENCY', 'EUR'),

    'currencies' => [
        'iso' => env('MONEY_ISO_CURRENCIES', true),
        'custom' => [],
        'allowed' => null,
    ],

    'schema' => [
        'currency_length' => env('MONEY_CURRENCY_LENGTH', 3),
        'precision' => env('MONEY_PRECISION', 38),
    ],

    'rounding' => env('MONEY_ROUNDING', 'half_away_from_zero'),

    'formatting' => [
        'driver' => env('MONEY_FORMATTER', 'auto'),
        'locale' => env('MONEY_LOCALE'),
        'display' => 'symbol',
        'fallback' => [
            'pattern' => '{sign}{amount} {code}',
            'decimal_separator' => '.',
            'thousands_separator' => ',',
        ],
    ],

    'macros' => [
        'collection' => true,
        'request' => true,
        'blade' => true,
        'validation' => true,
    ],

    'exchange' => [
        'default' => env('MONEY_EXCHANGE_DRIVER', 'ecb'),
        'chain' => ['database', 'ecb'],
        'pivot' => 'EUR',
        'rounding' => env('MONEY_EXCHANGE_ROUNDING', 'half_even'),
        'timezone' => env('MONEY_EXCHANGE_TIMEZONE', 'Europe/Berlin'),   // "today" for undated lookups
        'max_age_days' => env('MONEY_EXCHANGE_MAX_AGE_DAYS', 7),

        'cache' => [
            'enabled' => env('MONEY_EXCHANGE_CACHE', true),
            'store' => env('MONEY_EXCHANGE_CACHE_STORE'),
            'ttl' => env('MONEY_EXCHANGE_CACHE_TTL', 3600),
            'prefix' => 'money:exchange',
        ],

        'providers' => [
            'config' => [
                'rates' => [],   // ['EUR' => ['USD' => '1.0854']]
            ],
            'database' => [
                'table' => env('MONEY_EXCHANGE_TABLE', 'money_exchange_rates'),
                'model' => env('MONEY_EXCHANGE_MODEL', RoundlyConsulting\Money\Models\CurrencyRate::class),
            ],
            'ecb' => [
                'daily_url' => 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
                'recent_url' => 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml',
                'history_url' => 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml',
                'timeout' => env('MONEY_ECB_TIMEOUT', 10),
                'retries' => 2,
                'max_bytes' => 33554432,
                'cache_ttl' => 3600,
            ],
        ],

        'refresh' => [
            'schedule' => env('MONEY_EXCHANGE_SCHEDULE', false),
            'cron' => '30 16 * * 1-5',
            'timezone' => 'Europe/Berlin',
            'source' => 'ecb',
        ],
    ],
];

Every key

KeyDefaultEnvPurpose
default_currencyEURMONEY_DEFAULT_CURRENCYCurrency for Request::money() without one, the parser without a currency token (or with a shared symbol such as $ that this currency writes), and money_amount without a parameter.
currencies.isotrueMONEY_ISO_CURRENCIESSeed the registry with the bundled ISO 4217 list (165 currencies); false = custom only.
currencies.custom[]—Extra currencies: code => exponent (0..18), name, symbol.
currencies.allowednull—Allow-list for currencies chosen by input (null = the whole registry). Must be an array of codes. Never restricts stored data.
schema.currency_length3MONEY_CURRENCY_LENGTHVarchar length of currency columns (3..10) and the longest custom code. Fixed once migrated.
schema.precision38MONEY_PRECISIONP of the decimal(P, 0) amount columns (19..65); the cast and MoneyAmount enforce it. Fixed once migrated.
roundinghalf_away_from_zeroMONEY_ROUNDINGService-level default rounding (formatter digit reduction, avgMoney). Value objects never read config.
formatting.driverautoMONEY_FORMATTERauto (intl when loaded, else decimal), intl (fails loud without the extension) or decimal.
formatting.localenullMONEY_LOCALEnull follows app()->getLocale().
formatting.displaysymbol—Default currency display: symbol, code or none.
formatting.fallback.pattern{sign}{amount} {code}—Deterministic formatter pattern; placeholders {sign} {amount} {code} {symbol}.
formatting.fallback.decimal_separator.—Decimal separator — also the parser’s separator without intl. Blank = not set → '.'.
formatting.fallback.thousands_separator,—Thousands separator of the formatter and parser without intl. Any string. The one setting where blank is a value: ' ' groups with a space and '' means no grouping symbol; only null takes ','.
macros.collectiontrue—Register sumMoney / minMoney / maxMoney / avgMoney.
macros.requesttrue—Register Request::money().
macros.bladetrue—Register @money.
macros.validationtrue—Register the currency_code and money_amount string rules.
exchange.defaultecbMONEY_EXCHANGE_DRIVERDefault driver: config, database, ecb, chain or your own.
exchange.chain['database', 'ecb']—Drivers the chain driver tries, in order — a list of driver names, never chain itself.
exchange.pivotEUR—Triangulation currency for config and database. Not set (null or blank) = no pivot.
exchange.roundinghalf_evenMONEY_EXCHANGE_ROUNDINGDefault rounding of conversions.
exchange.timezoneEurope/BerlinMONEY_EXCHANGE_TIMEZONETimezone of “today” for undated lookups and undated manual rates, and of the returned rate dates. A requested date is always its own calendar day (Y-m-d in its own timezone).
exchange.max_age_days7MONEY_EXCHANGE_MAX_AGE_DAYSA newest rate older than this is stale and refused. At least 0 (0 accepts only the day’s own rate).
exchange.cache.enabledtrueMONEY_EXCHANGE_CACHECache database, ecb and custom driver lookups.
exchange.cache.storenullMONEY_EXCHANGE_CACHE_STORECache store (null = default).
exchange.cache.ttl3600MONEY_EXCHANGE_CACHE_TTLCache lifetime in seconds, at least 1 (switch caching off with cache.enabled).
exchange.cache.prefixmoney:exchange—Cache key prefix.
exchange.providers.config.rates[]—Static rates as decimal strings: ['EUR' => ['USD' => '1.0854']].
exchange.providers.database.tablemoney_exchange_ratesMONEY_EXCHANGE_TABLERates table.
exchange.providers.database.modelCurrencyRate::classMONEY_EXCHANGE_MODELSwappable rate model — CurrencyRate or a subclass; anything else throws InvalidMoneyConfiguration.
exchange.providers.ecb.daily_url / recent_url / history_urlecb.europa.eu—Daily, 90-day and full-history feeds.
exchange.providers.ecb.timeout10MONEY_ECB_TIMEOUTHTTP timeout in seconds, at least 1.
exchange.providers.ecb.retries2—Retries after the first attempt, at least 0.
exchange.providers.ecb.max_bytes33554432—Response size cap (32 MiB), at least 1.
exchange.providers.ecb.cache_ttl3600—Parsed-feed cache in seconds, at least 1.
exchange.refresh.schedulefalseMONEY_EXCHANGE_SCHEDULERegister the rate refresh schedule.
exchange.refresh.cron30 16 * * 1-5—When the schedule runs (weekdays at 16:30).
exchange.refresh.timezoneEurope/Berlin—Timezone of the cron expression.
exchange.refresh.sourceecb—Source the schedule refreshes from.

Environment

The common knobs are env-driven, so you rarely need to publish the config at all:

MONEY_DEFAULT_CURRENCY=EUR
MONEY_ROUNDING=half_away_from_zero
MONEY_FORMATTER=auto
MONEY_EXCHANGE_DRIVER=chain
MONEY_EXCHANGE_SCHEDULE=true
MONEY_EXCHANGE_MAX_AGE_DAYS=7

Macro toggles

Each developer-experience surface — collection macros, Request::money(), @money and the string validation rules — can be switched off under macros. An existing host macro with the same name always wins. The Blueprint macros have no toggle: published migrations depend on them.

On/off switches

The on/off switches — currencies.iso, macros.*, exchange.cache.enabled and exchange.refresh.schedule — accept true/false, 1/0, on/off or yes/no, from .env or the published file; anything else throws the package toolkit’s InvalidConfigurationException naming the key, so a typo never silently reads as the default.

Strict settings

Every other setting is read strictly too, at the point of use. A key that is not set — absent, null or blank ('' or whitespace, such as a MONEY_ROUNDING= line in .env) — takes its default; a present value of the wrong shape throws InvalidMoneyConfiguration naming the key, and is never cast or swapped for the default:

  • Integers take an int or a canonical integer string (“30”, “-5”) within the stated range — “five”, “5.5” or “1e3” throw.
  • Names — currency, driver, timezone, table, cache store and prefix, URLs, locale, pattern — must be strings.
  • The rounding modes, formatting.driver and formatting.display must name one of their values.
  • currencies.allowed, currencies.custom, exchange.chain and exchange.providers.config.rates must be arrays — a string allow-list such as 'EUR' throws rather than allowing every currency.
  • exchange.providers.database.model must be CurrencyRate or a subclass of it — anything else throws instead of falling back to the packaged model.

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 crypto

By 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.