tbbc/money-bundle
A Symfony bundle that integrates the moneyphp/money library into your application, offering Twig filters and extensions for formatting monetary values, form types for money input, and ready-to-use services for safe currency handling.
Install the bundle and enable it in Symfony:
composer require tbbc/money-bundle
// config/bundles.php
return [
// ...
Tbbc\MoneyBundle\TbbcMoneyBundle::class => ['all' => true],
];
Configure the currencies and reference currency. The exact config keys are version-dependent, but the common setup is:
# config/packages/tbbc_money.yaml
tbbc_money:
currencies: ['EUR', 'USD']
reference_currency: 'EUR'
exchange_rates:
USD: 1.10
Clear the cache and inspect what the bundle registered:
php bin/console cache:clear
php bin/console debug:container tbbc_money
The first practical use case is a product price: accept a Money object in a form, persist the amount and currency separately, and format it in Twig.
use Tbbc\MoneyBundle\Form\Type\MoneyType;
$builder->add('price', MoneyType::class, [
'currency' => 'USD',
]);
{{ product.price|money_format }}
{{ product.price|money_intl_format }}
Start by reading the bundle’s form type, Twig extension, and converter service definitions. Do not begin by coupling your entities directly to the bundle; treat it as an adapter around moneyphp/money.
Keep moneyphp Money in the domain, not in Doctrine.
Use integer minor units plus an ISO currency code in persistence. Reconstruct Money in the entity or a mapper.
use Money\Currency;
use Money\Money;
final class Product
{
private int $priceAmount;
private string $priceCurrency;
public function getPrice(): Money
{
return new Money($this->priceAmount, new Currency($this->priceCurrency));
}
public function setPrice(Money $price): void
{
$this->priceAmount = (int) $price->getAmount();
$this->priceCurrency = $price->getCurrency()->getCode();
}
}
#[ORM\Column(type: 'bigint')]
private int $priceAmount;
#[ORM\Column(length: 3)]
private string $priceCurrency;
Use the form type as the UI boundary.
For a fixed-currency field:
$builder->add('price', MoneyType::class, [
'currency' => 'USD',
]);
For a user-selectable currency, keep amount and currency as separate fields and combine them after validation:
$builder
->add('amount', MoneyType::class, ['currency' => 'USD'])
->add('currency', CurrencyType::class);
$money = new Money(
$form->get('amount')->getData()->getAmount(),
$form->get('currency')->getData()
);
Format only at the presentation edge.
Keep raw Money objects in services and controllers. Format in Twig or an API presenter.
{{ order.total|money_format }}
{{ order.total|money_intl_format }}
If the bundled Twig filter signature differs in your version, inspect the extension service with debug:container and use the formatter service directly in PHP.
Inject the formatter or converter rather than duplicating formatting logic.
use Tbbc\MoneyBundle\Formatter\MoneyFormatterInterface;
final class InvoicePresenter
{
public function __construct(private MoneyFormatterInterface $formatter)
{
}
public function present(Money $money): string
{
return $this->formatter->formatMoney($money);
}
}
Check debug:container for the actual service IDs and aliases. If interface autowiring is not configured, bind it explicitly:
services:
Tbbc\MoneyBundle\Formatter\MoneyFormatterInterface:
alias: tbbc_money.formatter.money_formatter
Use the bundle converter for display-rate conversions, not accounting.
use Money\Currency;
use Tbbc\MoneyBundle\Pair\PairConverterInterface;
final class PriceConverter
{
public function __construct(private PairConverterInterface $converter)
{
}
public function toEur(Money $usd): Money
{
return $this->converter->convert($usd, new Currency('EUR'));
}
}
Record the rate and timestamp externally if the converted value is shown later. Do not treat a config exchange rate as a settlement rate.
Serialize money explicitly.
Prefer a stable API shape over exposing the Money object:
{
"amount": 1299,
"currency": "USD",
"formatted": "$12.99"
}
or, if you must expose major units:
{
"amount": "12.99",
"currency": "USD"
}
Test with fixed rates and explicit locales.
Override the rate configuration in the test environment, format with a fixed locale, and assert integer minor units in assertions. Avoid tests that depend on live exchange providers or the host’s default locale.
Money amounts are integer minor units. Money::USD(1299) means $12.99; Money::USD(12.99) is wrong and may be coerced or rejected./100; use the formatter or the currency’s subunit.Money is immutable. multiply(), add(), and subtract() return new instances.Money object. Empty or null input often needs explicit empty_data handling or a nullable wrapper; otherwise transformers can throw.money_intl_format depends on the PHP intl extension and locale. Output can differ across environments and tests; pin the locale.debug:container and add aliases for MoneyFormatterInterface, PairConverterInterface, or rate-provider interfaces if needed.CurrencyType choices typically come from configuration. Adding a currency requires a config change and cache clear.Money object. Persist amount and currency columns; map explicitly.Money. Normalize to a documented API contract.How can I help you explore Laravel packages today?