dotdev/currency
PHP currency conversion component for objects, originally from the Sylius ecosystem. Provides a lightweight engine for converting between currencies in your applications. See Sylius docs for usage, contributions, and issue tracking.
Installation:
composer require dotdev/currency
Publish the configuration (if available):
php artisan vendor:publish --tag="currency-config"
Basic Usage:
Register the service provider in config/app.php:
Dotdev\Currency\CurrencyServiceProvider::class,
Use the currency converter in a controller or service:
use Dotdev\Currency\Converter\CurrencyConverterInterface;
class PaymentController extends Controller
{
public function __construct(private CurrencyConverterInterface $converter)
{
}
public function convert()
{
$amount = $this->converter->convert(100, 'USD', 'EUR');
return response()->json(['amount' => $amount]);
}
}
First Use Case: Convert a fixed amount between currencies (e.g., for a product price display):
$priceInUSD = 100;
$priceInEUR = $this->converter->convert($priceInUSD, 'USD', 'EUR');
src/Converter/CurrencyConverter.php for core logic.config/currency.php (if published) for default settings like base currency or rate sources.Service Integration:
Bind the CurrencyConverterInterface to a concrete implementation in Laravel’s service container:
$this->app->bind(
CurrencyConverterInterface::class,
function ($app) {
return new CurrencyConverter(
$app->make(RateProviderInterface::class),
$app->make(CurrencyRepositoryInterface::class)
);
}
);
Dynamic Rate Providers:
Implement a custom RateProviderInterface to fetch rates from an external API (e.g., ECB):
class EcbRateProvider implements RateProviderInterface
{
public function getRate(string $baseCurrency, string $targetCurrency): float
{
$response = Http::get("https://api.example.com/rates", [
'base' => $baseCurrency,
'target' => $targetCurrency,
]);
return $response->object()->rate;
}
}
Model Integration:
Attach currency conversion to Eloquent models (e.g., Product):
class Product extends Model
{
public function getPriceInCurrency(string $currency): float
{
return $this->converter->convert($this->price, $this->currency, $currency);
}
}
Request-Based Conversion: Convert amounts based on user-selected currency (e.g., from session/locale):
$userCurrency = session('currency', 'USD');
$productPrice = $product->getPriceInCurrency($userCurrency);
Checkout Flow:
API Responses:
return response()->json([
'price' => $this->converter->format($amount, $currency),
]);
Admin Dashboard:
Laravel Localization:
Use Laravel’s App::setLocale() to ensure currency symbols/formatters align with user locale:
App::setLocale($userLocale);
$formattedPrice = $this->converter->format($amount, $currency);
Caching Rates: Cache API-fetched rates in Redis to reduce latency:
$rate = Cache::remember(
"currency_rate_{$base}_{$target}",
now()->addHours(1),
fn() => $this->rateProvider->getRate($base, $target)
);
Fallback Rates: Provide fallback rates for unsupported currencies to avoid runtime errors:
try {
$rate = $this->rateProvider->getRate($base, $target);
} catch (UnsupportedCurrencyException $e) {
$rate = $this->fallbackRateProvider->getRate($base, $target);
}
Testing:
Mock the RateProviderInterface in tests to avoid external dependencies:
$this->mock(RateProviderInterface::class)
->shouldReceive('getRate')
->with('USD', 'EUR')
->andReturn(0.85);
Precision Loss:
bcmath or gmp for high precision:
$rate = bcdiv($amount, $rate, 6); // 6 decimal places
Unsupported Currencies:
if (!$this->currencyRepository->isSupported($currency)) {
throw new UnsupportedCurrencyException($currency);
}
Rate Source Dependencies:
Time Zone Sensitivity:
Archived Package Risks:
Log Conversions: Log conversion operations for auditability:
\Log::debug('Currency conversion', [
'amount' => $amount,
'from' => $fromCurrency,
'to' => $toCurrency,
'rate' => $rate,
'result' => $convertedAmount,
]);
Check Rate Sources: Verify rates are being fetched from the expected source (e.g., API, database):
\Log::info('Fetched rate', [
'base' => $baseCurrency,
'target' => $targetCurrency,
'rate' => $rate,
'source' => method_exists($provider, 'getSource') ? $provider->getSource() : 'unknown',
]);
Test Edge Cases:
null, non-numeric strings).Default Currency:
Ensure the default_currency in config matches your application’s primary currency to avoid silent failures.
Rate Update Frequency: If using a database-backed rate provider, schedule regular updates (e.g., via Laravel tasks):
// app/Console/Commands/UpdateCurrencyRates.php
public function handle()
{
$this->rateProvider->updateRates();
}
Register the command in app/Console/Kernel.php:
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->call('update:currency-rates');
}
Custom Formatters:
Extend the CurrencyFormatterInterface to add locale-specific formatting:
class CustomCurrencyFormatter implements CurrencyFormatterInterface
{
public function format(float $amount, string $currency): string
{
return number_format($amount, 2, ',', ' ') . ' ' . $currency;
}
}
Bulk Conversions: Add a method to convert multiple amounts at once (e.g., for batch processing):
public function convertBatch(array $amounts, string $fromCurrency, string $toCurrency): array
{
$rate = $this->getRate($fromCurrency, $toCurrency);
return array_map(fn($amount) => $amount * $rate, $amounts);
}
Currency-Specific Logic: Implement currency-specific rules (e.g., tax calculations) via strategy pattern:
class CurrencyStrategyFactory
{
public function create(string $currency): CurrencyStrategyInterface
{
return match ($currency) {
'USD' => new UsdTaxStrategy(),
'EUR' => new EurTaxStrategy(),
default => new DefaultTaxStrategy(),
};
}
}
Event Dispatching:
Dispatch events for currency conversions (e.g., CurrencyConverted) to trigger side effects:
event(new CurrencyConverted(
$amount,
$fromCurrency,
$toCurrency,
$convertedAmount
));
$rate = Cache::remember("user_{$userId}_rate_{$base}_{$target}", now()->
How can I help you explore Laravel packages today?