Installation:
composer require astina/money-bundle:dev-master
Add the bundle to config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 3):
Astina\Bundle\MoneyBundle\AstinaMoneyBundle::class => ['all' => true],
Database Setup:
Run migrations to create the currency_exchange_rate table:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
First Use Case: Create exchange rates via Doctrine:
$rate = new Astina\Bundle\MoneyBundle\Entity\CurrencyExchangeRate();
$rate->setFromCurrency('USD');
$rate->setToCurrency('EUR');
$rate->setRate(0.85); // 1 USD = 0.85 EUR
$em->persist($rate);
$em->flush();
Basic Conversion: Inject the converter service:
$money = new \Money\Money(100, 'USD');
$converter = $this->container->get('astina_money.money_converter');
$eurMoney = $converter->convert($money, 'EUR');
Currency Conversion:
MoneyConverter service for all conversions:
$converter->convert($money, 'JPY'); // Automatically fetches rate from DB
CurrencyExchangeRateNotFoundException by default).Entity Integration:
Money objects to Doctrine entities:
use Astina\Bundle\MoneyBundle\Type\MoneyType;
/**
* @ORM\Column(type="money")
*/
private $price;
MoneyType for form handling:
$builder->add('price', MoneyType::class, [
'currency' => 'USD',
'scale' => 2,
]);
Batch Processing:
$rates = $em->getRepository(CurrencyExchangeRate::class)
->findBy(['fromCurrency' => 'USD']);
$converter->setExchangeRates($rates); // Cache rates in memory
Money objects with custom JSON encoders:
$encoder = new \Astina\Bundle\MoneyBundle\Serializer\MoneyEncoder();
$serializer->encode($money, 'json');
$converter = $this->get('astina_money.money_converter');
$converted = $converter->convert($inputMoney, 'GBP');
money.conversion events to log/audit conversions:
$dispatcher->addListener('money.conversion', function ($event) {
// Log conversion details
});
Missing Exchange Rates:
if (!$converter->hasRate('USD', 'CAD')) {
throw new \RuntimeException("Exchange rate not configured");
}
$converter->setFallbackStrategy(function ($money, $toCurrency) {
return $money; // Return original if rate missing
});
Database Locking:
$em->beginTransaction();
try {
$rate = $em->find(CurrencyExchangeRate::class, $id);
$rate->setRate($newRate);
$em->flush();
$em->commit();
} catch (\Exception $e) {
$em->rollback();
throw $e;
}
Precision Loss:
scale in MoneyType to avoid floating-point errors:
$builder->add('price', MoneyType::class, [
'scale' => 4, // For micro-currency precision
]);
$converter->setDebug(true);
prePersist/preUpdate to validate Money objects:
$em->getEventManager()->addEventListener(
\Doctrine\ORM\Events::prePersist,
function ($event) {
$entity = $event->getObject();
if ($entity instanceof HasMoney) {
$entity->getMoney()->validate(); // Custom validation
}
}
);
Custom Rate Sources:
Astina\Bundle\MoneyBundle\Rate\RateProviderInterface:
class ApiRateProvider implements RateProviderInterface {
public function getRate(string $from, string $to): float {
return $this->fetchFromExternalApi($from, $to);
}
}
# config/services.yaml
Astina\Bundle\MoneyBundle\MoneyConverter:
arguments:
$rateProviders: ['@api_rate_provider']
Custom Money Types:
MoneyType for domain-specific validation:
class ProductPriceType extends MoneyType {
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'currency' => 'USD',
'min_value' => new Money(0, 'USD'),
]);
}
}
Fallback Strategies:
$converter->setFallbackStrategy(function ($money, $toCurrency) {
if ($toCurrency === 'XBT') { // Bitcoin
return $money->multiply(0.00005); // Arbitrary fallback
}
return $money;
});
config/packages/astina_money.yaml:
astina_money:
default_currency: 'USD'
rate_providers:
- '@database_rate_provider'
money DBAL type is registered:
$connection->getDatabasePlatform()->registerDoctrineTypeMapping('money', MoneyType::class);
How can I help you explore Laravel packages today?