symfgenus/money-bundle
symfgenus/money-bundle is a Symfony bundle for working with monetary values in a consistent, currency-aware way. It connects money objects with Symfony services, configuration, and templating, making price handling and formatting easier across your application.
Read the stack mismatch before you run composer require. symfgenus/money-bundle is a Symfony2 bundle: it registers Currency and Money services through a Symfony DI extension and Kernel::registerBundles(). Laravel has no bundle registration, no DI extension, no config tree at that layer. There is no service provider, no facade, no config/money.php, and no Flex recipe. Installing it in a Laravel app will pull in Symfony2-era constraints and fail dependency resolution on PHP 8.x; if it somehow resolves, you get nothing you can autowire via app()->make() in a way you would want to maintain.
So the first step is: treat it as a reference implementation, not a dependency. The real artifact you want is the Money value object it wires. In Laravel the maintained path is:
composer require moneyphp/money
Require ext-bcmath (or ext-gmp) and ext-intl in your composer.json, and confirm the platform provides them in CI and production — intl in particular is frequently missing from slim container images.
Where to look first. Skim the bundle source (it is small — expect a handful of service definitions plus a DependencyInjection/ extension) purely to see which services it exposes and how it scopes them. Then go to moneyphp/money and read Money, Currency, and Money::allocate. That is your actual day-to-day API.
Your first use case should be the narrowest one with a real payoff: a single price field rendered from integer minor units, or a payment total that currently goes through float. Do not start with splits, tax, or multi-currency reporting — those force policy decisions (rounding mode, allocation strategy) that deserve their own conversation with finance.
Minimum wiring in Laravel:
// app/Providers/MoneyServiceProvider.php
public function register(): void
{
$this->app->singleton(MoneyFormatter::class, fn () => new MoneyFormatter(
supported: config('money.currencies'), // ['USD', 'EUR', 'GBP']
default: config('money.default'), // 'USD'
));
}
The bundle's equivalent of that singleton is exactly what you are replacing, but with Laravel-native config publishing and a provider you own.
Persist as integer minor units plus an ISO 4217 currency column. That is the pattern the package exists to make easy on Symfony; on Laravel you implement it yourself.
Schema::create('orders', function (Blueprint $table) {
$table->bigInteger('total_amount'); // minor units, e.g. 1999 = $19.99
$table->char('total_currency', 3); // 'USD'
$table->timestamps();
});
Never float/double, never a bare decimal without a currency alongside it.
Wrap the two columns in an Eloquent cast so domain code only ever sees Money:
final class MoneyCast implements CastsAttributes
{
public function get($model, $key, $value, array $attributes): ?Money
{
$amount = $attributes["{$key}_amount"] ?? null;
$currency = $attributes["{$key}_currency"] ?? null;
return $amount === null
? null
: new Money((int) $amount, new Currency($currency));
}
public function set($model, $key, $value, array $attributes): array
{
return [
"{$key}_amount" => $value?->getAmount(),
"{$key}_currency" => $value?->getCurrency()->getCode(),
];
}
}
Format at the edge, never in the domain. A small presenter service built on NumberFormatter:
new MoneyFormatter($money->getCurrency()->getCode(), NumberFormatter::CURRENCY)
->format($money);
Keep this out of models and aggregates. The display string is a view concern; the Money object is the domain concern.
Validate at the boundary. A currency-code rule (Rule::in(array_keys(Currency::getCurrencies())) or an explicit allow-list from config) and an integer amount rule belong in form requests or FormRequest-level validation, not in the aggregate.
Allocate with Money::allocate, not division. Splitting a total across line items with a remainder is the case that silently breaks with floats:
[$a, $b, $c] = $orderTotal->allocate([1, 1, 1]);
// sums exactly to $orderTotal; remainder goes to the first buckets per the library's rule
Write golden-value tests around this — a one-cent drift is a financial event, not a test failure you triage later.
Keep the pattern at the domain boundary. Controllers, jobs, and listeners translate to/from Money; aggregates and policies use Money internally. This gives you one place to swap the implementation later (e.g. brick/money if FX enters scope).
When you genuinely need multi-currency or configurable context — FX conversion, per-currency exponents beyond the library defaults — brick/money is worth evaluating. moneyphp/money covers the common case and should not be over-rotated past its comfort zone.
The Symfony2 mismatch is the top gotcha. Do not spend a day trying to make the bundle autowire. Even on PHP 7.x it brings constraints designed for a framework generation that has been EOL for years, and on PHP 8.x it will fail Composer resolution or crash on load. If a legacy Symfony2 service genuinely consumes it, isolate that service behind an anti-corruption layer — no Money object from the old library should cross into your Laravel domain.
Verify the Composer name before trusting anything. The package name (symfgenus/money-bundle) and the repository (hanishsingla/TbbcMoneyBundle) differ; this looks like a fork. If you were to depend on it, you would need a VCS repositories entry rather than Packagist, and you would be the sole owner of any defect. Zero stars, zero dependents, and an abandoned ancestor library mean zero external validation.
The underlying library has been superseded. mathiasverraes/money was renamed/transferred to moneyphp/money (v3/v4). Any tutorial or Stack Overflow answer referencing the former maps onto the latter, but the bundle wraps the old one — do not copy bundle-era code expecting it to run against v4.
Never let floats touch the boundary. (float) $money->getAmount() inside a calculation is the exact silent-corruption path this whole pattern exists to prevent. Add a static-analysis rule (Psalm/PHPStan custom rule, or a grep in CI) to block reintroduction of floatval, number_format, and FLOAT/DOUBLE column types in money-adjacent code.
Currency exponents are not all two. JPY has zero, KWD/BHD/JOD have three. Money::getAmount() is always minor units, so a JPY amount of 1000 is ¥1000, not ¥10.00. NumberFormatter handles this on display; your validation and rounding logic must too. ISO 4217 is updated periodically — pin the currency list version and confirm it matches what finance expects.
Rounding mode is a product decision, not a default. moneyphp/money will throw on mixed-currency arithmetic (good — you want that to fail loud), but rounding on tax, discount, and allocation needs an explicit mode (half-up, banker's, ceiling). Document the chosen mode per surface and test each.
ext-intl is a runtime dependency, not just a dev one. Slim PHP images often ship without it; NumberFormatter will then either throw or produce silently wrong output depending on how you guard. Add it to your Dockerfile and to a composer check-platform-reqs step in CI.
Config and ownership. What the bundle gave a Symfony2 app for free — a container-scoped set of money services — you now own. Put the currency allow-list and default currency in config/money.php, publish it, and assign an owner in the team that owns payments/billing. The exit plan if you ever need to swap moneyphp/money for brick/money is a one-file change if and only if the pattern is confined to the domain boundary; that discipline is the real extension point.
How can I help you explore Laravel packages today?