ph-il/money-bundle
ph-il/money-bundle brings structured money handling to Laravel/PHP apps, with helpers for monetary values, currencies, formatting, and conversions. Use it to keep prices, totals, and currency logic consistent and reusable across your project.
Check your PHP and Symfony versions first
The 6.4 line requires PHP 8.4+ and Symfony 7.4 or 8.0. This is a hard floor, not a nice-to-have — on anything older, Composer will simply fail to resolve the constraint. Confirm both before you touch Composer:
php -v # must be >= 8.4
php bin/console --version # Symfony 7.4.x or 8.0.x
Note that v6.4.1 is described as "no real change" over 6.4.0 — it's a housekeeping release (it removes an unused requirement). Don't expect behavioural changes from it.
Install the package
composer require ph-il/money-bundle
This pulls in moneyphp/money as a dependency. Because moneyphp/money has its own PHP floor, cross-check the resolved version in composer.lock — the bundle's PHP 8.4 requirement and the library's requirement are separate constraints and both must be satisfied.
Register the bundle
If you're on Symfony Flex with a symfony.lock entry, the bundle is auto-registered. Otherwise add it manually in config/bundles.php:
Phil\MoneyBundle\PhilMoneyBundle::class => ['all' => true],
The organisation is moving. The release notes state the project will relocate to a new organisation with the next version. That means the Composer package name (ph-il/money-bundle) and possibly the namespace (Phil\MoneyBundle) may change. Confirm the current namespace against vendor/ph-il/money-bundle/src/ before wiring it in, and expect a follow-up migration step. Budget for a composer require new-vendor/money-bundle plus a namespace-wide rename in a future sprint — keep your own imports of bundle classes few and centralised.
Look here first
vendor/ph-il/money-bundle/README.md — still the primary documentation.vendor/ph-il/money-bundle/src/DependencyInjection/Configuration.php — the real config reference; every YAML key the bundle accepts is defined here.vendor/ph-il/money-bundle/src/Resources/config/services.xml (or .php) — tells you which service IDs are available to inject.First use case: format a price in Twig
Build a Money object using the library directly:
use Money\Money;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\IntlMoneyFormatter;
$money = new Money(1999, new \Money\Currency('USD')); // 19.99 USD, in minor units
Then either pass it to the view and format with a Twig extension if the bundle ships one, or format in the controller. Prefer injecting a bundle-provided formatter service over newing up IntlMoneyFormatter yourself — that keeps the intl extension and locale configured once.
Dump the container to see exactly what's registered:
bin/console debug:container --show-private | grep -i money
Inject the formatter, don't construct it. The value of a bundle is centralised configuration of Money\Formatter\DecimalMoneyFormatter / IntlMoneyFormatter and the Money\Currencies\Currencies implementation (usually ISOCurrencies). Injecting the service keeps currency lists consistent across the app and lets you swap ISOCurrencies for a custom currency set in one place.
Keep minor units at the boundary. Store and transport integer minor units (1999), convert to decimal strings only for display. If the bundle provides a Doctrine type, it almost certainly maps to BIGINT/INTEGER — confirm the column type in the bundle's Doctrine/ directory before migrating existing columns, since a mistaken DECIMAL(10,2) will silently misbehave.
Symfony Form integration. If a MoneyType form field is registered, use it with the divisor option so user input like 19.99 maps to 1999. This keeps parsing logic out of your controllers:
$builder->add('price', MoneyType::class, [
'currency' => 'USD',
// 'divisor' => 100,
]);
Conversion and exchange. For anything involving CurrencyPair, Money\Converter, or an exchange service, wrap the bundle's service in your own thin adapter (e.g. PriceConverter) rather than injecting it across the codebase. Given the organisation move on the horizon, a single call site for third-party money logic means a future namespace change or fork touches one class, not forty.
Testing. Assert on Money values, not strings:
$this->assertTrue($invoice->getTotal()->equals(Money::USD(1999)));
Where you need to assert against stored values, use getAmount() (integer) and getCurrency()->getCode() — comparing IntlMoneyFormatter output ties your tests to the ICU version installed on the CI machine.
Plan a migration window for v8.0. The release notes promise "lots of rewrite to move in a more modern way" in 8.0 (PHP 8.4+ / Symfony 8.0, and likely a new namespace under the new organisation). Keep your integration thin and documented so the 7.x → 8.0 upgrade is a contained change.
ph-il/money-bundle will not follow the project when it moves. Record the current version in composer.json, watch the project's repository for the new name, and re-test imports after any switch. Keep bundle-specific code in as few files as possible so the rename is mechanical.composer.json require block to see the exact constraint of whatever version you install."ph-il/money-bundle": "^6.4" or whatever tag resolves) rather than *, and commit composer.lock. With an advertised 8.0 rewrite inbound, ^6.4 gives you patch-level fixes without silently pulling a breaking 7.x.new Money(19.99, ...) will fail or behave unexpectedly — moneyphp accepts integers only (or numeric strings on newer versions). Every "the number is 100× too big" bug traces back to this. Cast and validate before constructing.ISOCurrencies knows that JPY has 0 decimals and KWD has 3. Never hardcode /100. Use Money\Money::getAmount() plus the currency's subunit count when converting manually.intl extension required. IntlMoneyFormatter needs ext-intl. Minimal Docker images (e.g. php:8.4-alpine) often lack it — Composer installs fine, formatting blows up at runtime. Add intl to your image build.IntlMoneyFormatter respects the \NumberFormatter locale, so $19.99 becomes 19,99 $ under fr_FR. Set locale per request for user-facing output; use DecimalMoneyFormatter for logs and APIs.doctrine:schema:validate may flag existing columns. Register the type explicitly in doctrine.dbal.types if the bundle doesn't do it via a compiler pass — otherwise Unknown column type "money" appears only on first hydration.default_currency key exists — check Configuration.php. If it's absent, define your default currency in your own parameters and inject it rather than expecting the bundle to fill the gap.Currencies service. Override it in services.yaml (same service ID, your implementation) if you need a restricted currency list — safer than forking, and much safer given the imminent re-organisation.vendor/ as usual and rely on your own typed adapters around bundle classes.bin/console debug:container Money\Money to locate the service; bin/console lint:container catches wiring errors before runtime; if a compiler pass fails silently, run with -vvv after bin/console cache:clear.moneyphp/money library is well-maintained and independent of the bundle. You can always drop the bundle and wire the two or three services you use directly in services.yaml. Given the organisation move and 8.0 rewrite, plan the integration so this remains a half-day change rather than a refactor.How can I help you explore Laravel packages today?