“這個世界上有很大比例的計算機都在操縱金錢,因此,我一直感到困惑的是,金錢實際上並不是任何主流程式語言中的一流資料型別。 缺乏型別會導致問題,這是最明顯的周邊貨幣。 如果您所有的計算都是用一種貨幣完成的,那麼這並不是一個大問題,但是一旦涉及多種貨幣,您就希望避免在不考慮貨幣差異的情況下將美元加到日元中。 更細微的問題是舍入。 貨幣計算通常四捨五入為最小的貨幣單位。 執行此操作時,由於舍入錯誤,很容易損失幾美分(或您當地的等值貨幣)“ -- 馬丁福勒的貨幣設計模式(Flower's money pattern)
為什麼要使用貨幣類
相比於使用 float 類來儲存貨幣的傳統做法,使用貨幣類(Money library)可以照顧到處理金錢的所有細微複雜之處,比如四捨五入、金額計算、匯率換算、貨幣格式化輸出等
貨幣模式在 Laravel 中的使用
<?php
namespace App\Http\Controllers;
use App\Support\Money;
class TestController extends Controller
{
public function index()
{
// 從容器中新建一個價值為 1000 美元的貨幣類, 其單位為「分」
$money = app(Money::class, [100000, 'USD']);
// 格式化輸出
$money->value(); // 1000.00
$money->format(); // $1,000.00
// 貨幣運算
$money2 = app(Money::class, [5000, 'USD']);
$money->add($money2)->format(); // $1,050.00
$money->divide(10)->format(); // $100.00
// 根據實時匯率換算為人民幣
$money->convertTo('CNY')->format(); // CN¥7,028.70
// 根據固定匯率換算
$exchangeRate = [
'USD' => [
'EUR' => 0.7,
'CNY' => 8.7654321,
],
];
$money->fixedConvertTo('EUR', $exchangeRate)->format(); // €700.00
$money->fixedConvertTo('CNY', $exchangeRate)->format(); // CN¥8,765.43
// 從字串解析貨幣
Money::parse('$300')->format(); // $300.00
}
}
在 Laravel 中實現貨幣模式
cknow/laravel-money
moneyphp/money
是貨幣模式的主要實現,cknow/laravel-money
在其之上做了 Laravel 框架的擴充
安裝
$ composer require cknow/laravel-money
釋出配置檔案
$ php artisan vendor:publish --provider="Cknow\Money\MoneyServiceProvider"
內容如下:
# config/money.php
<?php
return [
/*
|--------------------------------------------------------------------------
| Laravel money
|--------------------------------------------------------------------------
*/
'locale' => config('app.locale', 'en_US'), // 預設地區
'currency' => config('app.currency', 'USD'), // 預設幣種
];
florianv/laravel-swap
florianv/swap
支援請求多種三方 API 獲取匯率,並寫入快取,最終完成貨幣的匯率換算。florianv/laravel-swap
是對 florianv/swap
在 Laravel 框架的封裝
安裝
$ composer require florianv/laravel-swap
釋出配置檔案
$ php artisan vendor:publish --provider="Swap\Laravel\SwapServiceProvider"
# config/swap.php
<?php
......
return [
// 使用哪種快取驅動
'cache' => 'redis',
// 快取過期時間
'options' => [
'cache_ttl' => 12 * 60 * 60,
],
// 匯率 API, 我使用的是 currency_converter, 需要在此處填入你自己的服務商祕鑰
'services' => [
'currency_converter' => [
'access_key' => 'key',
'enterprise' => false,
],
],
];
Money Service Provider
cknow/laravel-money
和 florianv/laravel-swap
的使用方式有些繁瑣。通過 Laravel 的服務容器可以簡化許多操作。
# app/Providers/MoneyServiceProvider.php
<?php
namespace App\Providers;
use Money\Currency;
use Money\Converter;
use App\Support\Money;
use Money\Exchange\SwapExchange;
use Money\Exchange\FixedExchange;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\DecimalMoneyFormatter;
use Illuminate\Contracts\Foundation\Application;
use Cknow\Money\MoneyServiceProvider as CknowMoneyServiceProvider;
class MoneyServiceProvider extends CknowMoneyServiceProvider
{
public function boot()
{
// 為貨幣類設定預設地區
Money::setLocale(config('money.locale'));
// 為貨幣類設定預設幣種
Money::setCurrency(config('money.currency'));
}
public function register()
{
/*
|--------------------------------------------------------------------------
| 繫結 ISO Currency
|--------------------------------------------------------------------------
|
| ISO 標準貨幣幣種列表
|
*/
$this->app->singleton(ISOCurrencies::class, static function (Application $app) {
return new ISOCurrencies();
});
$this->app->alias(ISOCurrencies::class, 'iso_currencies');
/*
|--------------------------------------------------------------------------
| 繫結 Currency
|--------------------------------------------------------------------------
|
| 貨幣幣種
|
*/
$this->app->bind(Currency::class, static function (Application $app, array $parameters = []) {
$currencyCode = current($parameters);
return new Currency($currencyCode ?: \Cknow\Money\Money::getCurrency());
});
$this->app->alias(Currency::class, 'currency');
/*
|--------------------------------------------------------------------------
| 繫結 Fixed Exchange
|--------------------------------------------------------------------------
|
| 固定匯率貨幣換算服務
|
*/
$this->app->bind(FixedExchange::class, static function (Application $app, array $exchangeRate = []) {
return new FixedExchange($exchangeRate);
});
$this->app->alias(FixedExchange::class, 'fixed_exchange');
/*
|--------------------------------------------------------------------------
| 繫結 Swap Exchange
|--------------------------------------------------------------------------
|
| 動態匯率貨幣換算服務
|
*/
$this->app->singleton(SwapExchange::class, static function (Application $app) {
return new SwapExchange($app->get('swap'));
});
$this->app->alias(SwapExchange::class, 'swap_exchange');
/*
|--------------------------------------------------------------------------
| 繫結 Fixed Converter
|--------------------------------------------------------------------------
|
| 固定匯率貨幣換算器
|
*/
$this->app->bind('fixed_converter', static function (Application $app, array $exchangeRate = []) {
return new Converter($app->get('iso_currencies'), $app->make('fixed_exchange', $exchangeRate));
});
/*
|--------------------------------------------------------------------------
| 繫結 Swap Converter
|--------------------------------------------------------------------------
|
| 動態匯率貨幣換算器
|
*/
$this->app->singleton('swap_converter', static function (Application $app) {
return new Converter($app->get('iso_currencies'), $app->get('swap_exchange'));
});
/*
|--------------------------------------------------------------------------
| 繫結 Decimal Money Formatter
|--------------------------------------------------------------------------
|
| 預設的貨幣列印格式 - 小數格式
|
*/
$this->app->singleton(DecimalMoneyFormatter::class, static function (Application $app) {
return new DecimalMoneyFormatter(app('iso_currencies'));
});
$this->app->alias(DecimalMoneyFormatter::class, 'decimal_money_formatter');
/*
|--------------------------------------------------------------------------
| 繫結 Money
|--------------------------------------------------------------------------
|
| 貨幣類
|
*/
$this->app->bind(Money::class, static function (Application $app, array $parameters = []) {
$amount = current($parameters);
$currencyCode = next($parameters);
$currency = $app->make(Currency::class, [$currencyCode]);
return new Money($amount, $currency);
});
$this->app->alias(Money::class, 'money');
}
}
封裝自己的貨幣類
根據自己的情況實現一些方法,方便使用
# app/Support/Money.php
<?php
namespace App\Support;
use Money\Currency;
use Cknow\Money\Money as BaseMoney;
use Money\Formatter\DecimalMoneyFormatter;
class Money extends BaseMoney
{
/**
* 根據動態匯率換算金額.
*
* @param string $currency 要換算的幣種
*
* @return self
*/
public function convertTo(string $currency): self
{
$money = app('swap_converter')->convert($this->money, app(Currency::class, [$currency]));
return $this->rebuild($money);
}
/**
* 根據固定匯率換算金額.
*
* @param string $currency 要換算的幣種
* @param array $exchangeRate 自定義的匯率
*
* @return self
*/
public function fixedConvertTo(string $currency, array $exchangeRate): self
{
$money = app('fixed_converter', $exchangeRate)->convert($this->money, app(Currency::class, [$currency]));
return $this->rebuild($money);
}
/**
* 以小數形式輸出金額.
*
* @return string
*/
public function value(): string
{
return app(DecimalMoneyFormatter::class)->format($this->money);
}
/**
* 將 \Money\Money 類轉換為 \App\Support\Money 類.
*
* @param \Money\Money $money
*
* @return self
*/
protected function rebuild($money): self
{
$currencyCode = $money->getCurrency()->getCode();
$amount = $money->getAmount();
return app(self::class, [$amount, $currencyCode]);
}
/**
* 重寫父類魔術方法, 在存入資料庫等操作時自動轉換為小數.
*
* @return string
*/
public function __toString()
{
return $this->value();
}
}
到目前為止,便可以實現文章一開始的貨幣操作、匯率換算、列印功能
在 Eloquent Model 中使用貨幣類
貨幣在資料庫中一般以浮點數的形式儲存,使用 Eloquent Model 的 cast
特性,可以在把資料取出來時就把浮點數形式的貨幣轉換為貨幣類
vkovic/laravel-custom-casts
vkovic/laravel-custom-casts
可以將 Eloquent Model 的屬性(資料庫的欄位)自動轉換成自定義的類
安裝
$ composer require vkovic/laravel-custom-casts
新建 Money Cast
#app/Models/Casts/MoneyCast.php
<?php
namespace App\Models\Casts;
use App\Support\Money;
use Vkovic\LaravelCustomCasts\CustomCastBase;
class MoneyCast extends CustomCastBase
{
/**
* 屬性存入資料庫時轉換為浮點數.
*
* @param Money $money
*
* @return string
*/
public function setAttribute($money)
{
if (! $money instanceof Money) {
return $money;
}
return $money->divide(100)->value();
}
/**
* 取出屬性時轉換為 Money 類.
*
* @param $money
*
* @return Money
*/
public function castAttribute($money): Money
{
$money = bcmul($money, 100, 4);
return app(Money::class, [$money]); // 在我的專案中預設幣種為 USD, 所以這裡省略了第二個引數
}
}
在 moneyphp/money
中貨幣的基本單位為「分」,但我的專案中貨幣的基本單位為「元」,所以我在取出 / 存入貨幣時分別乘以 / 除以了 100。
在模型中引入
# app/Models/Order.php
<?php
namespace App\Models;
use App\Models\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;
use Vkovic\LaravelCustomCasts\HasCustomCasts;
class Order extends Model
{
use HasCustomCasts;
public $casts = [
'order_total' => MoneyCast::class // 將訂單總金額自動轉為貨幣類
];
}
使用
<?php
namespace App\Http\Controllers;
use App\Support\Money;
class TestController extends Controller
{
public function index()
{
$order = Order::query()->first(); // 該訂單的金額在資料庫中為 67.84
dump($order->order_total); // 該欄位已自動從浮點數轉為了貨幣類
dump($order->order_total->format());
}
}
# 結果
App\Support\Money {#1677 ▼
#money: Money\Money {#1753 ▼
-amount: "6784"
-currency: Money\Currency {#1682 ▼
-code: "USD"
}
}
#attributes: []
}
"$67.84"
其他
擴充包 moneyphp/money 還提供了許多其他有用的方法,可以看下它的官方文件
我在使用這個擴充包時根據專案的情況做了一些封裝處理,如有不足或更好的實現歡迎指出