azaharizaman/nexus-currency 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

azaharizaman/nexus-currency

Composer 安装命令:

composer require azaharizaman/nexus-currency

包简介

Framework-agnostic ISO 4217-compliant currency management and exchange rate engine for the Nexus ERP system

README 文档

README

License: MIT PHP Version

Framework-agnostic ISO 4217-compliant currency management and exchange rate engine for the Nexus ERP system.

Overview

The Nexus\Currency package provides authoritative currency metadata and exchange rate management following ISO 4217 standards. It complements the existing Nexus\Finance\ValueObjects\Money implementation by providing validation, formatting, and exchange rate coordination without replacing core financial value objects.

Key Features

  • ISO 4217 Compliance: Full support for international currency standards
  • Decimal Precision Rules: Correct handling of 0-decimal (JPY), 2-decimal (USD), and 3-decimal (BHD) currencies
  • Exchange Rate Management: Pluggable provider architecture for external rate APIs
  • Intelligent Caching: Configurable rate storage to minimize API calls
  • Framework-Agnostic: Pure PHP with no Laravel dependencies
  • BCMath Compatible: Designed to work with high-precision monetary calculations
  • Stateless Design: All services are stateless and horizontally scalable

Architecture

This package follows the "Logic in Packages, Implementation in Applications" pattern:

┌─────────────────────────────────────────────────────────────┐
│                    Nexus\Currency                           │
│  (ISO 4217 Authority & Exchange Rate Coordination)          │
└────────────────────┬────────────────────────────────────────┘
                     │ provides metadata
                     ↓
┌─────────────────────────────────────────────────────────────┐
│                    Nexus\Finance                            │
│         (Money & ExchangeRate Value Objects)                │
└────────────────────┬────────────────────────────────────────┘
                     │ used by
                     ↓
┌─────────────────────────────────────────────────────────────┐
│  Nexus\Accounting, Nexus\Payroll, Nexus\Procurement, etc.  │
└─────────────────────────────────────────────────────────────┘

Integration with Nexus\Finance

The Nexus\Currency package augments (not replaces) existing financial components:

  • Nexus\Finance\ValueObjects\Money remains the core monetary value object
  • Nexus\Finance\ValueObjects\ExchangeRate remains the core rate value object
  • Nexus\Currency provides:
    • Currency metadata (symbols, names, decimal rules)
    • Currency code validation
    • Exchange rate provider coordination
    • Formatting utilities

What This Package Provides

  • CurrencyManager: High-level currency operations (validation, formatting, metadata)
  • ExchangeRateService: Exchange rate lookup with caching and conversion
  • Value Objects: Currency (metadata), CurrencyPair (pair representation)
  • Contracts: Repository, Provider, and Storage interfaces
  • Exceptions: Domain-specific errors with static factories

What the Application Must Implement

The consuming application (Nexus\Atomy) must provide:

  1. Currency Repository: Database-backed implementation with ISO 4217 seed data
  2. Exchange Rate Provider: Integration with external APIs (ECB, Fixer.io, etc.) using Nexus\Connector
  3. Rate Storage: Redis/Database caching implementation
  4. Service Bindings: IoC container bindings in service provider

Installation

composer require azaharizaman/nexus-currency:"*@dev"

Requirements

  • PHP 8.3 or higher
  • BCMath extension (for precision calculations)
  • PSR-3 Logger implementation (optional)

Core Components

1. Currency Value Object

Immutable representation of ISO 4217 currency metadata:

use Nexus\Currency\ValueObjects\Currency;

// Currency with full ISO 4217 data
$usd = new Currency(
    code: 'USD',
    name: 'US Dollar',
    symbol: '$',
    decimalPlaces: 2,
    numericCode: '840'
);

// Access metadata
echo $usd->getCode();          // "USD"
echo $usd->getSymbol();        // "$"
echo $usd->getDecimalPlaces(); // 2

// Format amounts
echo $usd->formatAmount('1234.56');              // "$ 1,234.56"
echo $usd->formatAmount('1234.56', false, true); // "1,234.56 USD"

// Check decimal type
$jpy = new Currency('JPY', 'Japanese Yen', '¥', 0, '392');
$jpy->isZeroDecimal(); // true

2. CurrencyPair Value Object

Represents a currency exchange pair:

use Nexus\Currency\ValueObjects\CurrencyPair;

// Create pair
$pair = new CurrencyPair('USD', 'EUR');

// Or from string notation
$pair = CurrencyPair::fromString('USD/EUR');

// Access components
echo $pair->getFromCode(); // "USD"
echo $pair->getToCode();   // "EUR"
echo $pair->toString();    // "USD/EUR"

// Get inverse
$inverse = $pair->inverse(); // EUR/USD

3. CurrencyManager Service

High-level currency management:

use Nexus\Currency\Services\CurrencyManager;

$manager = app(CurrencyManager::class);

// Get currency metadata
$usd = $manager->getCurrency('USD');

// Validate currency code
$manager->validateCode('USD'); // void (success)
$manager->validateCode('XXX'); // throws CurrencyNotFoundException

// Check existence
$manager->exists('EUR'); // true
$manager->exists('ZZZ'); // false

// Get decimal precision
$precision = $manager->getDecimalPrecision('JPY'); // 0

// Format amounts
$formatted = $manager->formatAmount('1234.5678', 'USD');
// "$ 1,234.57" (rounded to 2 decimals)

// Get all currencies
$all = $manager->getAllCurrencies();

// Search currencies
$results = $manager->searchCurrencies('dollar');

4. ExchangeRateService

Exchange rate lookup and conversion:

use Nexus\Currency\Services\ExchangeRateService;
use Nexus\Currency\ValueObjects\CurrencyPair;
use Nexus\Finance\ValueObjects\Money;

$service = app(ExchangeRateService::class);

// Get current exchange rate
$pair = new CurrencyPair('USD', 'EUR');
$rate = $service->getRate($pair); // Returns Nexus\Finance\ValueObjects\ExchangeRate

// Get historical rate
$date = new DateTimeImmutable('2024-01-15');
$historicalRate = $service->getRate($pair, $date);

// Convert money
$usd = Money::of(100, 'USD');
$eur = $service->convert($usd, 'EUR'); // Money in EUR

// Get multiple rates (batch operation)
$pairs = [
    new CurrencyPair('USD', 'EUR'),
    new CurrencyPair('USD', 'GBP'),
    new CurrencyPair('USD', 'JPY'),
];
$rates = $service->getRates($pairs);

// Refresh specific rates (bypass cache)
$service->refreshRates($pairs);

// Clear all cached rates
$service->clearCache();

// Check provider capabilities
$service->supportsHistoricalRates(); // true/false
$service->getProviderName();         // "ECB" or "Fixer.io"
$service->isProviderAvailable();     // true/false

Application Integration

Step 1: Implement Currency Repository

Create a database-backed repository in apps/Atomy:

namespace App\Repositories;

use Nexus\Currency\Contracts\CurrencyRepositoryInterface;
use Nexus\Currency\ValueObjects\Currency;
use Illuminate\Support\Facades\DB;

class DbCurrencyRepository implements CurrencyRepositoryInterface
{
    public function findByCode(string $code): ?Currency
    {
        $row = DB::table('currencies')
            ->where('code', $code)
            ->where('is_active', true)
            ->first();

        if (!$row) {
            return null;
        }

        return new Currency(
            code: $row->code,
            name: $row->name,
            symbol: $row->symbol,
            decimalPlaces: $row->decimal_places,
            numericCode: $row->numeric_code
        );
    }

    public function getAll(): array
    {
        $rows = DB::table('currencies')
            ->where('is_active', true)
            ->get();

        $currencies = [];
        foreach ($rows as $row) {
            $currencies[$row->code] = new Currency(
                code: $row->code,
                name: $row->name,
                symbol: $row->symbol,
                decimalPlaces: $row->decimal_places,
                numericCode: $row->numeric_code
            );
        }

        return $currencies;
    }

    public function exists(string $code): bool
    {
        return DB::table('currencies')
            ->where('code', $code)
            ->where('is_active', true)
            ->exists();
    }

    // Implement other methods...
}

Step 2: Create Database Migration

// database/migrations/xxxx_create_currencies_table.php
Schema::create('currencies', function (Blueprint $table) {
    $table->string('code', 3)->primary();
    $table->string('name', 100);
    $table->string('symbol', 10);
    $table->tinyInteger('decimal_places')->default(2);
    $table->string('numeric_code', 3);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Step 3: Seed ISO 4217 Data

// database/seeders/CurrencySeeder.php
DB::table('currencies')->insert([
    ['code' => 'USD', 'name' => 'US Dollar', 'symbol' => '$', 'decimal_places' => 2, 'numeric_code' => '840'],
    ['code' => 'EUR', 'name' => 'Euro', 'symbol' => '', 'decimal_places' => 2, 'numeric_code' => '978'],
    ['code' => 'GBP', 'name' => 'British Pound', 'symbol' => '£', 'decimal_places' => 2, 'numeric_code' => '826'],
    ['code' => 'JPY', 'name' => 'Japanese Yen', 'symbol' => '¥', 'decimal_places' => 0, 'numeric_code' => '392'],
    ['code' => 'MYR', 'name' => 'Malaysian Ringgit', 'symbol' => 'RM', 'decimal_places' => 2, 'numeric_code' => '458'],
    ['code' => 'SGD', 'name' => 'Singapore Dollar', 'symbol' => 'S$', 'decimal_places' => 2, 'numeric_code' => '702'],
    ['code' => 'CNY', 'name' => 'Chinese Yuan', 'symbol' => '¥', 'decimal_places' => 2, 'numeric_code' => '156'],
    ['code' => 'BHD', 'name' => 'Bahraini Dinar', 'symbol' => 'BD', 'decimal_places' => 3, 'numeric_code' => '048'],
    // Add more currencies as needed...
]);

Step 4: Implement Exchange Rate Provider

Using Nexus\Connector for resilient API integration:

namespace App\Services\ExchangeRates;

use Nexus\Currency\Contracts\ExchangeRateProviderInterface;
use Nexus\Currency\ValueObjects\CurrencyPair;
use Nexus\Finance\ValueObjects\ExchangeRate;
use Nexus\Connector\Services\ConnectorManager;
use DateTimeImmutable;

class EcbExchangeRateProvider implements ExchangeRateProviderInterface
{
    public function __construct(
        private readonly ConnectorManager $connector
    ) {}

    public function getRate(CurrencyPair $pair, ?DateTimeImmutable $asOf = null): ExchangeRate
    {
        // Use Nexus\Connector with circuit breaker and retry logic
        $response = $this->connector->execute(
            connectionId: 'ecb-api',
            method: 'GET',
            endpoint: $asOf 
                ? "/history/{$asOf->format('Y-m-d')}" 
                : '/latest',
            params: [
                'base' => $pair->getFromCode(),
                'symbols' => $pair->getToCode(),
            ]
        );

        $data = json_decode($response->getBody(), true);

        if (!isset($data['rates'][$pair->getToCode()])) {
            throw ExchangeRateNotFoundException::forPair($pair, $asOf);
        }

        return ExchangeRate::create(
            fromCurrency: $pair->getFromCode(),
            toCurrency: $pair->getToCode(),
            rate: $data['rates'][$pair->getToCode()],
            effectiveDate: $asOf ?? new DateTimeImmutable()
        );
    }

    public function supportsHistoricalRates(): bool
    {
        return true;
    }

    public function getProviderName(): string
    {
        return 'European Central Bank';
    }

    public function isAvailable(): bool
    {
        return $this->connector->isHealthy('ecb-api');
    }

    // Implement getRates()...
}

Step 5: Implement Rate Storage (Redis)

namespace App\Services\ExchangeRates;

use Nexus\Currency\Contracts\RateStorageInterface;
use Nexus\Currency\ValueObjects\CurrencyPair;
use Nexus\Finance\ValueObjects\ExchangeRate;
use Illuminate\Support\Facades\Redis;
use DateTimeImmutable;

class RedisRateStorage implements RateStorageInterface
{
    private const PREFIX = 'exchange_rate:';

    public function get(CurrencyPair $pair, ?DateTimeImmutable $asOf = null): ?ExchangeRate
    {
        $key = $this->buildKey($pair, $asOf);
        $data = Redis::get($key);

        if (!$data) {
            return null;
        }

        $decoded = json_decode($data, true);

        return ExchangeRate::create(
            fromCurrency: $decoded['from'],
            toCurrency: $decoded['to'],
            rate: $decoded['rate'],
            effectiveDate: new DateTimeImmutable($decoded['date'])
        );
    }

    public function put(CurrencyPair $pair, ExchangeRate $rate, int $ttl = 3600): bool
    {
        $key = $this->buildKey($pair, $rate->getEffectiveDate());
        $data = json_encode([
            'from' => $rate->getFromCurrency(),
            'to' => $rate->getToCurrency(),
            'rate' => $rate->getRate(),
            'date' => $rate->getEffectiveDate()->format('Y-m-d H:i:s'),
        ]);

        return Redis::setex($key, $ttl, $data);
    }

    public function forget(CurrencyPair $pair, ?DateTimeImmutable $asOf = null): bool
    {
        $key = $this->buildKey($pair, $asOf);
        return Redis::del($key) > 0;
    }

    public function flush(): bool
    {
        $keys = Redis::keys(self::PREFIX . '*');
        return count($keys) > 0 ? Redis::del($keys) > 0 : true;
    }

    public function has(CurrencyPair $pair, ?DateTimeImmutable $asOf = null): bool
    {
        $key = $this->buildKey($pair, $asOf);
        return Redis::exists($key) > 0;
    }

    private function buildKey(CurrencyPair $pair, ?DateTimeImmutable $asOf): string
    {
        $dateStr = $asOf ? $asOf->format('Y-m-d') : 'current';
        return self::PREFIX . $pair->toString() . ':' . $dateStr;
    }
}

Step 6: Bind Implementations in Service Provider

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Nexus\Currency\Contracts\CurrencyRepositoryInterface;
use Nexus\Currency\Contracts\ExchangeRateProviderInterface;
use Nexus\Currency\Contracts\RateStorageInterface;
use Nexus\Currency\Contracts\CurrencyManagerInterface;
use Nexus\Currency\Services\CurrencyManager;
use Nexus\Currency\Services\ExchangeRateService;
use App\Repositories\DbCurrencyRepository;
use App\Services\ExchangeRates\EcbExchangeRateProvider;
use App\Services\ExchangeRates\RedisRateStorage;

class CurrencyServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind repository
        $this->app->singleton(
            CurrencyRepositoryInterface::class,
            DbCurrencyRepository::class
        );

        // Bind exchange rate provider
        $this->app->singleton(
            ExchangeRateProviderInterface::class,
            EcbExchangeRateProvider::class
        );

        // Bind rate storage
        $this->app->singleton(
            RateStorageInterface::class,
            RedisRateStorage::class
        );

        // Bind currency manager
        $this->app->singleton(
            CurrencyManagerInterface::class,
            CurrencyManager::class
        );

        // Bind exchange rate service
        $this->app->singleton(ExchangeRateService::class);
    }
}

Integration with Nexus\Finance

Enhancing Money Validation

Update Nexus\Finance\ValueObjects\Money to use CurrencyManager:

// In Nexus\Finance\ValueObjects\Money
use Nexus\Currency\Contracts\CurrencyManagerInterface;

private function validateCurrency(string $currency): void
{
    // Get CurrencyManager from container (or inject if refactoring to service)
    $currencyManager = app(CurrencyManagerInterface::class);
    
    // Delegate validation to Currency package
    try {
        $currencyManager->validateCode($currency);
    } catch (\Nexus\Currency\Exceptions\InvalidCurrencyCodeException $e) {
        throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
    } catch (\Nexus\Currency\Exceptions\CurrencyNotFoundException $e) {
        throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
    }
}

Currency-Aware Formatting

Add a formatting method to Money that uses currency metadata:

// In Nexus\Finance\ValueObjects\Money
public function formatWithCurrency(CurrencyManagerInterface $currencyManager): string
{
    return $currencyManager->formatAmount(
        amount: $this->amount,
        currencyCode: $this->currency,
        includeSymbol: true,
        includeCode: false
    );
}

Usage:

$money = Money::of(1234.5678, 'USD');
$currencyManager = app(CurrencyManagerInterface::class);

echo $money->formatWithCurrency($currencyManager); // "$ 1,234.57"

$jpy = Money::of(1234.5678, 'JPY');
echo $jpy->formatWithCurrency($currencyManager); // "¥ 1,235" (zero decimals)

Decimal Precision Strategy

The 4-Decimal Internal Standard

Nexus\Finance\ValueObjects\Money uses 4 decimal places for all internal BCMath calculations:

// In Money VO
private const PRECISION = 4;

Why 4 decimals internally?

  • Prevents rounding errors in complex calculations (tax, interest, multi-step conversions)
  • Allows accurate intermediate results for financial operations
  • Industry standard for accounting systems

Currency-Specific Display Precision

Nexus\Currency provides the correct display precision per ISO 4217:

$usd = $currencyManager->getCurrency('USD');
$usd->getDecimalPlaces(); // 2

$jpy = $currencyManager->getCurrency('JPY');
$jpy->getDecimalPlaces(); // 0

$bhd = $currencyManager->getCurrency('BHD');
$bhd->getDecimalPlaces(); // 3 (Bahraini Dinar)

Best Practice: Calculate at 4, Display per Currency

// Internal calculation (4 decimals)
$subtotal = Money::of(100.00, 'USD');
$tax = $subtotal->multiply(0.06); // 6.0000
$total = $subtotal->add($tax);    // 106.0000

// Display with currency-specific precision (2 decimals for USD)
$formatted = $total->formatWithCurrency($currencyManager);
// Result: "$ 106.00"

// For JPY (0 decimals)
$jpyTotal = Money::of(1234.5678, 'JPY');
$formatted = $jpyTotal->formatWithCurrency($currencyManager);
// Result: "¥ 1,235" (rounded, no decimals)

Error Handling

All exceptions provide static factory methods for contextual error messages:

use Nexus\Currency\Exceptions\CurrencyNotFoundException;
use Nexus\Currency\Exceptions\InvalidCurrencyCodeException;
use Nexus\Currency\Exceptions\ExchangeRateNotFoundException;
use Nexus\Currency\Exceptions\ExchangeRateProviderException;

try {
    $currency = $manager->getCurrency('XXX');
} catch (CurrencyNotFoundException $e) {
    // "Currency with code 'XXX' not found. Ensure it exists in the currency repository."
}

try {
    $manager->validateCode('12');
} catch (InvalidCurrencyCodeException $e) {
    // "Currency code must be 3 characters, got 2: '12'"
}

try {
    $rate = $service->getRate(new CurrencyPair('USD', 'FAKE'));
} catch (ExchangeRateNotFoundException $e) {
    // "Exchange rate not found for currency pair USD/FAKE..."
}

try {
    $rate = $service->getRate(new CurrencyPair('USD', 'EUR'));
} catch (ExchangeRateProviderException $e) {
    // "Exchange rate provider 'ECB' API failed. Please try again later."
}

Testing

Unit Testing with In-Memory Repository

For package unit tests, create a simple in-memory implementation:

use Nexus\Currency\Contracts\CurrencyRepositoryInterface;
use Nexus\Currency\ValueObjects\Currency;

class InMemoryCurrencyRepository implements CurrencyRepositoryInterface
{
    private array $currencies = [];

    public function __construct()
    {
        // Seed with common currencies for testing
        $this->currencies = [
            'USD' => new Currency('USD', 'US Dollar', '$', 2, '840'),
            'EUR' => new Currency('EUR', 'Euro', '', 2, '978'),
            'JPY' => new Currency('JPY', 'Japanese Yen', '¥', 0, '392'),
            'MYR' => new Currency('MYR', 'Malaysian Ringgit', 'RM', 2, '458'),
        ];
    }

    public function findByCode(string $code): ?Currency
    {
        return $this->currencies[$code] ?? null;
    }

    public function getAll(): array
    {
        return $this->currencies;
    }

    public function exists(string $code): bool
    {
        return isset($this->currencies[$code]);
    }

    // Implement other methods...
}

Documentation

📚 Complete Documentation

🔗 Quick Links

Documentation Description
Prerequisites System requirements and dependencies
Core Concepts ISO 4217, non-breaking augmentation, caching
Configuration Steps Complete setup guide
Laravel Integration Laravel migrations, repositories, service providers
Symfony Integration Symfony entities, services, configuration
Testing Examples Unit and integration test examples
Troubleshooting Common issues and solutions

License

MIT License - see LICENSE file for details.

Credits

Developed by the Nexus Development Team for the Nexus ERP System.

Related Packages

azaharizaman/nexus-currency 适用场景与选型建议

azaharizaman/nexus-currency 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 2 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 04 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 azaharizaman/nexus-currency 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 azaharizaman/nexus-currency 我们能提供哪些服务?
定制开发 / 二次开发

基于 azaharizaman/nexus-currency 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 2
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 41
  • 依赖项目数: 6
  • 推荐数: 3

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-04