moneyphp/money-bundle
Symfony bundle integrating the Money PHP library, providing service configuration, a Twig extension, form type, and validator for handling monetary values and currencies safely across your application.
Read this first: moneyphp/money-bundle is a Symfony bundle, not a Laravel package. It is a small dependency-injection wrapper around moneyphp/money that registers Money\Currency, a zero-amount Money\Money, and (as of v0.1.0) an ISO Money\Currencies service from a default_currency config value. There is nothing in it that a Laravel application can consume — no service provider, no facade, no config publish, and symfony/framework-bundle is not in your dependency tree.
So for a Laravel codebase the practical starting point is:
composer require moneyphp/money
That library is the actual value: an immutable Money value object, ISO currency handling, parsers, formatters, allocation, and conversion. The bundle only matters if you are on Symfony and want those three services autowired.
If you are in a Symfony app, the bundle's full setup is:
composer require moneyphp/money-bundle
# config/packages/money.yaml
money:
default_currency: EUR
// config/bundles.php
MoneyBundle\MoneyBundle::class => ['all' => true],
Then Money\Currency, a zero-amount Money\Money, and Money\Currencies (backed by ISOCurrencies) can be autowired. Before building anything on top, open vendor/moneyphp/money-bundle/src/Resources/config/services.xml and DependencyInjection/MoneyExtension.php — it takes two minutes and tells you the entire surface area of the package. As of v0.1.0 that is three service definitions and one config key.
First use case (Laravel): model an order total as an integer minor-unit column plus a currency column, and hydrate a Money object at the boundary.
use Money\Money;
use Money\Currency;
$price = new Money(1999, new Currency('USD')); // $19.99
$price->getAmount(); // 1999
$price->getCurrency()->getCode(); // "USD"
1. Store minor units, never decimals. BIGINT for the amount, CHAR(3) for the currency, and pair them on the same table. Never store a DECIMAL and convert back and forth — that is where rounding bugs live.
Schema::create('orders', function (Blueprint $table) {
$table->bigInteger('total_amount');
$table->char('total_currency', 3);
});
2. Use an Eloquent cast to keep Money out of your attributes array.
class MoneyCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): ?Money
{
if ($value === null) {
return null;
}
return new Money(
(int) $value,
new Currency($attributes[$key.'_currency'] ?? config('money.default_currency'))
);
}
public function set($model, string $key, $value, array $attributes): array
{
if (! $value instanceof Money) {
throw new InvalidArgumentException("{$key} must be a Money instance.");
}
return [
$key => $value->getAmount(),
$key.'_currency' => $value->getCurrency()->getCode(),
];
}
}
3. Bind the infrastructure singletons in a service provider. Currencies, formatters, and parsers are stateless and cheap to reuse. This is the Laravel equivalent of what the bundle now does in services.xml — one singleton() call each, no bundle required:
$this->app->singleton(Currencies::class, fn () => new ISOCurrencies());
$this->app->singleton(DecimalMoneyFormatter::class); // "19.99"
$this->app->singleton(IntlMoneyFormatter::class, fn ($app) => new IntlMoneyFormatter(
new \NumberFormatter('en_US', \NumberFormatter::CURRENCY),
$app->make(Currencies::class)
));
Use DecimalMoneyFormatter in API resources (machine output) and IntlMoneyFormatter in Blade (human output).
4. Parse user input, don't cast it. Form input arrives as "19.99" strings; (int) "19.99" is 19.
$money = (new DecimalMoneyParser($currencies))->parse($request->input('amount'), $request->input('currency'));
5. Allocate for splitting, not division.
Money::USD(100)->allocate([1, 1, 1]); // 34, 33, 33 — no cents lost
6. Conversion goes through Money\Converter with an exchange-rate provider (a fixed map for tests, your own adapter for production rates). Do not multiply getAmount() by a float yourself.
7. In Symfony, autowire Money\Currencies and Money\Currency freely, but inject a factory/service for real amounts — see the gotcha below about the autowired Money\Money. If you need non-ISO currencies (crypto, loyalty points), decorate or replace the newly-registered Currencies service rather than rebuilding it at each call site.
^0.1), and don't hard-code service IDs in compiled container extensions — read the XML in Resources/config/ instead, because the service names and definitions have already changed once between the initial shape and this release.Money\Money service is amount 0. It exists so you can inject a Money, not your money. If you __construct(Money $money) and expect a real value, you get zero and a very quiet bug. Inject Money\Currency or build Money from your own data.Currencies service is ISO-only. v0.1.0 adds a service definition of ISOCurrencies; it will happily reject or mis-scale a BTC/points currency. If you already bind your own Money\Currencies in a Laravel container, the bundle's addition changes nothing for you — but in Symfony, check which definition wins when you also register your own.==. Two Money instances with the same amount but distinct Currency objects can surprise you. Always use $a->equals($b). Comparisons (greaterThan, lessThan) also assert matching currencies and throw otherwise.ISOCurrencies knows this; hardcoded /100 math does not. Use DecimalMoneyParser/DecimalMoneyFormatter so scale is derived from the currency.Money from a float. new Money(19.99, $currency) passes a float into an int parameter, which is silently truncated under non-strict types. Type-hint int and cast at the DB boundary only.BIGINT on 32-bit PHP. Eloquent will hand back a string for values exceeding PHP_INT_MAX; the (int) cast in the snippet above will overflow silently. On 64-bit this is a non-issue for realistic amounts, but keep the cast explicit and validate upper bounds on input.$money->add($fee); with no assignment) is the single most common bug with this library.ratio(), allocate(), and average() each define their own remainder handling and there is no global "round half even" switch. If finance signs off on a specific rounding mode, test it explicitly rather than assuming — and consider brick/money if you need configurable rounding everywhere.Money leak into json_encode. Serialize explicitly in API resources (['amount' => $m->getAmount(), 'currency' => $m->getCurrency()->getCode()]) so your wire format is a deliberate contract rather than whatever the object happens to expose. Same for __toString() — don't rely on it.dump($money->getAmount(), $money->getCurrency()->getCode()). Mismatches are almost always a currency mismatch or a scale assumption, not arithmetic.money.currency in services.yaml to change the default, and override the Currencies service to add non-ISO currencies. In Laravel the equivalent is simply binding your own Currencies implementation in the container — one line, no bundle required.How can I help you explore Laravel packages today?