This bundle is used to integrate the Money library into a Symfony project.
This library is based on Fowler's Money pattern
- This bundle is tested and is stable with Symfony 6.4, 7.4 and 8.0
use Money\Money;
use Tbbc\MoneyBundle\Form\Type\MoneyType;
use Tbbc\MoneyBundle\Pair\PairManagerInterface;
public function convertAction(PairManagerInterface $pairManager): Response
{
// the money library
$fiveEur = Money::EUR(500);
$tenEur = $fiveEur->add($fiveEur);
[$part1, $part2, $part3] = $tenEur->allocate([1, 1, 1]);
assert($part1->equals(Money::EUR(334)));
assert($part2->equals(Money::EUR(333)));
assert($part3->equals(Money::EUR(333)));
// a service that stores conversion ratios
$usd = $pairManager->convert($tenEur, 'USD');
// a form integration
$formBuilder->add('price', MoneyType::class);
}- Integrates money library from Mathias Verraes
- Twig filters and PHP helpers for helping with money and currencies in templates
- A storage system for currency ratios
- A ratioProvider system for fetching ratio from externals api
- Symfony form integration
- Console commands for different operations
- A configuration parser for specifying website used currencies
- Access to the history of currency ratio fetched
- Money formatter i18n
Use Composer and install with
composer require tbbc/money-bundleAdd the bundle to config/bundles.php (if it was not automatically added during the installation of the package):
return [
// ...
Tbbc\MoneyBundle\TbbcMoneyBundle::class => ['all' => true],
];Create a file like config/packages/tbbc_money.yml and add it there:
tbbc_money:
currencies: ["USD", "EUR"]
reference_currency: "EUR"
decimals: 2In your config.yml or config/packages/tbbc_money.yml, add the form fields presentations:
twig:
form_themes:
- '@TbbcMoney/Form/fields.html.twig'You should also register custom Doctrine Money type:
doctrine:
dbal:
types:
money: Tbbc\MoneyBundle\Type\MoneyTypeTbbcMoneybundle comes with docker compose to ease development, to build dev docker containers, run:
docker compose up --build -dAnd once it is built, enter the php 8.2, 8.3, 8.4 or 8.5 container:
docker exec -it tbbc-moneybundle-php82 bash
docker exec -it tbbc-moneybundle-php83 bash
docker exec -it tbbc-moneybundle-php84 bash
docker exec -it tbbc-moneybundle-php85 bashAll the tooling is added as composer scripts, start by installing deps:
composer installThen you can use the code style (cs) commands to check/fix issues, rector to run the automatic refactors, or phpunit.
composer cs-check
composer cs-fix
composer rector-check
composer rector-fix
composer phpunit
composer phpstanTo generate a new baseline for phpstan:
composer phpstan-generate-baselineuse Money\Money;
$fiveEur = Money::EUR(500);
$tenEur = $fiveEur->add($fiveEur);
[$part1, $part2, $part3] = $tenEur->allocate([1, 1, 1]);
assert($part1->equals(Money::EUR(334)));
assert($part2->equals(Money::EUR(333)));
assert($part3->equals(Money::EUR(333)));
$pair = new CurrencyPair(new Currency('EUR'), new Currency('USD'), 1.2500);
$usd = $pair->convert($tenEur);
$this->assertEquals(Money::USD(1250), $usd);You have 3 new form types (under Tbbc\MoneyBundle\Form\Type namespace):
- CurrencyType : asks for a currency among currencies defined in config.yml
- MoneyType : asks for an amount and a currency
- SimpleMoneyType : asks for an amount and sets the currency to the reference currency set in config.yml
Example :
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
// I create my form
$form = $this->createFormBuilder()
->add('name', TextType::class)
->add('price', MoneyType::class, [
'data' => Money::EUR(1000), //EUR 10
])
->add('save', SubmitType::class)
->getForm();Manipulating the form
With MoneyType you can manipulate the form elements with
amount_options for the amount field, and currency_options for the currency field, fx if you want to change the label.
$form = $this->createFormBuilder()
->add('price', MoneyType::class, [
'data' => Money::EUR(1000), //EUR 10
'amount_options' => [
'label' => 'Amount',
],
'currency_options' => [
'label' => 'Currency',
],
])
->getForm();With CurrencyType only currency_options can be used, and with SimpleMoneyType only amount_options can be used.
Note that there are 2 columns in the DB table : $priceAmount and $priceCurrency and only one getter/setter : getPrice and setPrice.
The get/setPrice methods are dealing with these two columns transparently.
- Advantage : your DB is clean and you can do sql sum, group by, sort,... with the amount and the currency in two different columns in your db
- Disadvantage : it is ugly in the entity.
<?php
namespace App\AdministratorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Money\Currency;
use Money\Money;
#[ORM\Table(name: 'test_money')]
#[ORM\Entity]
class TestMoney
{
#[ORM\Column(type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id;
#[ORM\Column]
private int $priceAmount;
#[ORM\Column(length: 64)]
private string $priceCurrency;
public function getId(): int
{
return $this->id;
}
public function getPrice(): Money
{
if (!$this->priceCurrency) {
return null;
}
if (!$this->priceAmount) {
return new Money(0, new Currency($this->priceCurrency));
}
return new Money($this->priceAmount, new Currency($this->priceCurrency));
}
public function setPrice(Money $price): self
{
$this->priceAmount = $price->getAmount();
$this->priceCurrency = $price->getCurrency()->getCode();
return $this;
}
}There is only one string column in your DB table. The money object is manually serialized by the new Doctrine type.
1.25€ is serialized in your DB by 'EUR 125'. This format is stable. It won't change in future releases..
The new Doctrine type name is "money".
- Advantage : The entity is easy to create and use
- Disadvantage : it is more difficult to directly request the db in SQL.
<?php
namespace App\AdministratorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Money\Money;
#[ORM\Table(name: 'test_money')]
#[ORM\Entity]
class TestMoney
{
#[ORM\Column(type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id;
#[ORM\Column(type: 'money')]
private Money $price;
public function getId(): int
{
return $this->id;
}
public function getPrice(): Money
{
return $this->price;
}
public function setPrice(Money $price): self
{
$this->price = $price;
return $this;
}
}Convert an amount into another currency
use Tbbc\MoneyBundle\Pair\PairManagerInterface;
public function convertAction(PairManagerInterface $pairManager): Response
{
$usd = $pairManager->convert($amount, 'USD');
// ...
}Save a conversion value in a DB
use Money\Money;
use Tbbc\MoneyBundle\Pair\PairManagerInterface;
public function saveRatioAction(PairManagerInterface $pairManager): Response
{
// save in ratio file in CSV
$pairManager->saveRatio('USD', 1.25);
$eur = Money::EUR(100);
$usd = $pairManager->convert($eur, 'USD');
$this->assertEquals(Money::USD(125), $usd);
}<?php
namespace My\Controller\IndexController;
use Money\Money;
use Money\Currency;
use Tbbc\MoneyBundle\Formatter\MoneyFormatter;
use Symfony\Component\HttpFoundation\Response;
class IndexController
{
public function myAction(MoneyFormatter $moneyFormatter): Response
{
$price = new Money(123456789, new Currency('EUR'));
// best method (added in 2.2+ version)
\Locale::setDefault('fr_FR');
$formatedPrice = $moneyFormatter->localizedFormatMoney($price);
// 1 234 567,89 €
$formatedPrice = $moneyFormatter->localizedFormatMoney($price, 'en');
// €1,234,567.89
// old method (before v2.2)
$formattedPrice = $moneyFormatter->formatMoney($price);
// 1 234 567,89
$formattedCurrency = $moneyFormatter->formatCurrency($price);
// €
}
}{{ $amount | money_localized_format('fr') }} => 1 234 567,89 €
{{ $amount | money_localized_format('en_US') }} => €1,234,567.89
{{ $amount | money_localized_format }} => depends on your default locale
{{ $amount | money_format }}
{{ $amount | money_as_float }}
{{ $amount | money_get_currency | currency_symbol }}
{{ $amount | money_get_currency | currency_name }}
{{ $amount | money_convert("USD") | money_format }}
{{ $amount | money_format_currency }}# save a ratio in the storage
./bin/console tbbc:money:ratio-save USD 1.25
# display ratio list
./bin/console tbbc:money:ratio-list
./bin/console tbbc:money:ratio-list --format=table
./bin/console tbbc:money:ratio-list --format=json
# fetch all the ratio for all defined currencies from an external API
./bin/console tbbc:money:ratio-fetchThe ratio provider by default is base on the service Tbbc\MoneyBundle\Pair\RatioProvider\ECBRatioProvider.
- The ECBRatioProvider ratio provider is based on the data provided here https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml
You can write your own ratio provider by creating and custom class that implements the RatioProviderInterface interface.
namespace App\Money;
use Tbbc\MoneyBundle\Pair\RatioProviderInterface;
final class YourRatioProviderService implements RatioProviderInterface
{
public function fetchRatio(string $referenceCurrencyCode, string $currencyCode): float
{
// implement your custom logic here
}
}You can change the service to use in the config/packages/tbbc_money.yaml file :
tbbc_money:
ratio_provider: App\Money\YourRatioProviderServiceThis project integrates https://github.com/florianv/exchanger library to work with currency exchange rates from various services.
Installation:
composer require "florianv/exchanger" "php-http/message" "php-http/guzzle7-adapter"`Configuration:
tbbc_money:
ratio_provider: Tbbc\MoneyBundle\Pair\RatioProvider\ExchangerAdapterRatioProviderAdd to your crontab :
1 0 * * * /my_app_dir/bin/console tbbc:money:ratio-fetch > /dev/null
Create a money object from a float can be a bit tricky because of rounding issues.
use Tbbc\MoneyBundle\Money\MoneyManagerInterface;
public function createMoneyAction(MoneyManagerInterface $moneyManager): Response
{
$money = $moneyManager->createMoneyFromFloat('2.5', 'USD');
$this->assertEquals("USD", $money->getCurrency()->getCode());
$this->assertEquals(250, $money->getAmount());Doctrine is required to use this feature.
In order to get the ratio history, you have to enable it in the configuration and to use Doctrine.
tbbc_money:
currencies: ["USD", "EUR"]
reference_currency: "EUR"
enable_pair_history: trueThen you can use the service :
use Tbbc\MoneyBundle\PairHistory\PairHistoryManagerInterface;
public function historyAction(PairHistoryManagerInterface $pairHistoryManager): Response
{
$dt = new \DateTime("2023-07-08 11:14:15.638276");
// returns ratio for at a given date
$ratio = $pairHistoryManager->getRatioAtDate('USD', $dt);
// returns the list of USD ratio (relative to the reference value)
$ratioList = $pairHistoryManager->getRatioHistory('USD', $startDate, $endDate);
}Three storages for storing ratios are available : CSV File (csv), Doctrine ORM (doctrine), or MongoDB (document)
By default, TbbcMoneyBundle is configured with CSV File.
If you want to switch to a Doctrine storage, edit your config.yml
tbbc_money:
storage: doctrineUpdate your database schema:
If you're using DoctrineMigrationsBundle (recommended way):
./bin/console bin/console make:migration
./bin/console bin/console doctrine:migrations:migrateWithout DoctrineMigrationsBundle:
./bin/console doctrine:schema:update --forceWith the Doctrine storage, currency ratio will use the default entity manager and will store data inside the tbbc_money_doctrine_storage_ratios
The MoneyFormatter::localizedFormatMoney ( service 'tbbc_money.formatter.money_formatter' ) use the php NumberFormatter class ( http://www.php.net/manual/en/numberformatter.formatcurrency.php ) to format money.
You can :
- give your own \NumberFormatter instance as a parameter of MoneyFormatter::localizedFormatMoney
- subclass the MoneyFormatter and rewrite the getDefaultNumberFormatter method to set a application wide NumberFormatter
You have to disable the pair history service in order to use the TbbcMoneyBundle without Doctrine ORM or MongoDB.
tbbc_money:
enable_pair_history: trueNote : you can imagine to code your own PairHistoryManager for Propel, it is very easy to do. Don't hesitate to submit a PR with your code and your tests.
In your config.yml, you can :
- define the decimals count after a unit (ex : 12.25€ : 2 decimals ; 11.5678€ : 4 decimals)
tbbc_money:
currencies: ["USD", "EUR"]
reference_currency: "EUR"
decimals: 2
enable_pair_history: true
ratio_provider: tbbc_money.ratio_provider.yahoo_finance- Take a look at the list of issues.
- Fork
- Write a test (for either new feature or bug)
- Optional: Start MongoDB in Docker for local testing
docker run --name "money-bundle-mongo-db-test" -p 27017:27017 -d mongo:latest- Make a PR
Philippe Le Van - kitpages.fr - twitter : @plv
Thomas Tourlourat - Wozbe - twitter: @armetiz
