archtechx/money

一个用于处理PHP中货币数学的轻量级包。

v0.5.1 2024-03-29 01:12 UTC

This package is auto-updated.

Last update: 2024-08-29 01:59:44 UTC


README

一个用于处理货币的简单包。

主要功能

  • 简单的API
  • Livewire集成
  • 支持自定义货币
  • 高度可定制的格式化
  • 符合会计标准的舍入逻辑

此包是我们对货币模式的实现。

您可以在我们的论坛上了解更多关于我们为什么要构建它以及它是如何工作的: 新包:archtechx/money

安装

通过composer要求该包

composer require archtechx/money

使用方法

该包有两个主要类

  • Money 表示货币值
  • Currency 是您使用的货币的扩展

本文档使用了十进制值基数值默认货币当前货币舍入数学小数显示小数等术语。请参阅术语部分以获取定义。

Money

重要:作为货币模式的实现,Money对象在每次操作后都会创建一个新的实例。这意味着,所有Money实例都是不可变的。要修改变量的值,请使用新值重新初始化它

// Incorrect
$money = money(1500);
$money->times(3); // ❌
$money->value(); // 1500

// Correct
$money = money(1500);
$money = $money->times(3); // ✅
$money->value(); // 4500

创建Money实例

// Using cents
$money = money(1500); // $15.00; default currency
$money = money(1500, 'EUR'); // 15.00 €
$money = money(2000, new USD); // $20.00
$money = money(3000, CZK::class); // 30 Kč

// Using decimals
$money = Money::fromDecimal(15.00, 'EUR'); // 15.00 €
$money = Money::fromDecimal(20.00, new USD); // $20.00
$money = Money::fromDecimal(30.00, CZK::class); // 30 Kč

算术运算

// Addition
$money = money(1000);
$money = $money->add(500);
$money->value(); // 1500

// Subtraction
$money = money(1000);
$money = $money->subtract(500);
$money->value(); // 500

// Multiplication
$money = money(1000);
$money = $money->multiplyBy(2); // alias: ->times()
$money->value(); // 2000

// Division
$money = money(1000);
$money = $money->divideBy(2);
$money->value(); // 500

将货币转换为不同的货币

$money = money(2200);
$money->convertTo(CZK::class);

比较货币实例

货币值的相等性

// Assuming CZK is 25:1 USD

// ✅ true
money(100, USD::class)->equals(money(100, USD::class));

// ❌ false
money(100, USD::class)->equals(money(200, USD::class));

// ✅ true
money(100, USD::class)->equals(money(2500, CZK::class));

// ❌ false
money(100, USD::class)->equals(money(200, CZK::class));

货币值和货币的相等性

// Assuming CZK is 25:1 USD

// ✅ true
money(100, USD::class)->is(money(100, USD::class));

// ❌ false: different monetary value
money(100, USD::class)->is(money(200, USD::class));

// ❌ false: different currency
money(100, USD::class)->is(money(2500, CZK::class));

// ❌ false: different currency AND monetary value
money(100, USD::class)->is(money(200, CZK::class));

添加费用

您可以使用addFee()addTax()方法向货币添加百分比费用

$money = money(1000);
$money = $money->addTax(20.0); // 20%
$money->value(); // 1200

访问十进制值

$money = Money::fromDecimal(100.0, new USD);
$money->value(); // 10000
$money->decimal(); // 100.0

格式化货币

您可以使用->formatted()方法来格式化货币。它考虑了显示小数

$money = Money::fromDecimal(40.25, USD::class);
$money->formatted(); // $40.25

此方法可选地接受对货币指定的重写

$money = Money::fromDecimal(40.25, USD::class);

// $ 40.25 USD
$money->formatted(decimalSeparator: ',', prefix: '$ ', suffix: ' USD');

重写也可以作为数组传递

$money = Money::fromDecimal(40.25, USD::class);

// $ 40.25 USD
$money->formatted(['decimalSeparator' => ',', 'prefix' => '$ ', 'suffix' => ' USD']);

如果您希望使用数学小数而不是显示小数,还有->rawFormatted()

$money = Money::new(123456, CZK::class);
$money->rawFormatted(); // 1 234,56 Kč

将格式化的值转换回Money实例也是可能的。该包尝试从提供的字符串中提取货币

$money = money(1000);
$formatted = $money->formatted(); // $10.00
$fromFormatted = Money::fromFormatted($formatted);
$fromFormatted->is($money); // true

如果您在格式化货币实例时传递了重写,则可以将其传递给此方法。

$money = money(1000);
$formatted = $money->formatted(['prefix' => '$ ', 'suffix' => ' USD']); // $ 10.00 USD
$fromFormatted = Money::fromFormatted($formatted, USD::class, ['prefix' => '$ ', 'suffix' => ' USD']);
$fromFormatted->is($money); // true

说明

  1. 如果没有指定货币并且没有货币与前缀和后缀匹配,则会抛出异常。
  2. 如果没有指定货币并且多个货币使用相同的前缀和后缀,则会抛出异常。
  3. fromFormatted() 在小数大于 显示小数 时会遗漏分。

货币四舍五入

某些货币,例如捷克克朗(CZK),通常以整克朗显示最终价格,但在中间数学运算中使用分。例如

$money = Money::fromDecimal(3.30, CZK::class);
$money->value(); // 330
$money->formatted(); // 3 Kč

$money = $money->times(3);
$money->value(); // 990
$money->formatted(); // 10 Kč

如果客户购买一个单价为 3.30 的商品,他支付 3 CZK,但如果他购买三个单价为 3.30 的商品,他支付 10 CZK

这种(四舍五入到整克朗)是标准且合法的,按照会计法规,因为它使得支付更容易。然而,法律要求你记录四舍五入差异以供税务目的使用。

获取使用的四舍五入

为此,我们的包允许您通过简单的调用方法来获取四舍五入差异

$money = Money::fromDecimal(9.90, CZK::class);
$money->decimal(); // 9.90
$money->formatted(); // 10 Kč
$money->rounding(); // +0.10 Kč = 10

$money = Money::fromDecimal(3.30, CZK::class);
$money->decimal(); // 3.30
$money->formatted(); // 3 Kč
$money->rounding(); // -0.30 Kč = -30

将四舍五入应用于货币

// Using the currency rounding
$money = Money::fromDecimal(9.90, CZK::class);
$money->decimal(); // 9.90
$money = $money->rounded(); // currency rounding
$money->decimal(); // 10.0

// Using custom rounding
$money = Money::fromDecimal(2.22, USD::class);
$money->decimal(); // 2.22
$money = $money->rounded(1); // custom rounding: 1 decimal
$money->decimal(); // 2.20

货币

要处理已注册的货币,请使用绑定的 CurrencyManager 实例,您可以使用 currencies() 辅助函数访问它。

创建货币

此包默认仅提供 USD 货币。

您可以使用多种支持的语法之一来创建货币。

// anonymous Currency object
$currency = new Currency(
    code: 'FOO',
    name: 'Foo currency',
    rate: 1.8,
    prefix: '# ',
    suffix: ' FOO',
);

// array
$currency = [
    'code' => 'FOO',
    'name' => 'Foo currency',
    'rate' => 1.8,
    'prefix' => '# ',
    'suffix' => ' FOO',
];

// class
class FOO extends Currency
{
    protected string $code = 'FOO';
    protected string $name = 'Foo currency';
    protected float $rate = 1.8;
    protected string $prefix = '# ';
    protected string $suffix = ' FOO';
}

有关可配置的属性列表,请参阅 货币逻辑 部分。请注意,在注册货币时,必须指定两个值 必须

  1. 货币代码(例如 USD
  2. 货币名称(例如 美元

添加货币

注册新货币

currencies()->add(new USD);
currencies()->add(USD::class);
currencies()->add($currency); // object or array

移除特定货币

要移除特定货币,您可以使用 remove() 方法

currencies()->remove('USD');
currencies()->remove(USD::class);

移除所有货币

要移除所有货币,您可以使用 clear() 方法

currencies()->clear();

重置货币

在测试中可能很有用。这会撤销您的所有更改,并使 CurrencyManager 使用 USD 作为默认货币。

currencies()->reset();

货币逻辑

货币可以有以下属性

protected string $code = null;
protected string $name = null;
protected float $rate = null;
protected string $prefix = null;
protected string $suffix = null;
protected int $mathDecimals = null;
protected int $displayDecimals = null;
protected int $rounding = null;
protected string $decimalSeparator = null;
protected string $thousandsSeparator = null;

对于每个属性,都有一个相应的 public 方法。指定方法在您的货币配置是动态的时很有用,例如当货币汇率是从某个API获取时

public function rate(): float
{
    return cache()->remember("{$this->code}.rate", 3600, function () {
        return Http::get("https://api.currency.service/rate/USD/{$this->code}");
    });
}

设置默认货币

您可以使用 setDefault() 方法设置 默认货币

currencies()->setDefault('USD');

设置当前货币

您可以使用 setCurrent() 方法设置 当前货币

currencies()->setCurrent('USD');

在请求之间持久化选定的货币

如果您的用户可以选择他们想要在应用程序中看到的货币,该包可以自动将当前货币写入您选择的持久存储,并在后续请求中从该存储中读取。

例如,如果我们想使用 currency 会话密钥来跟踪用户选择的会话。要实现这一点,我们只需要这样做

currencies()
    ->storeCurrentUsing(fn (string $code) => session()->put('currency', $code))
    ->resolveCurrentUsing(fn () => session()->get('currency'));

您可以将此代码添加到 AppServiceProvider 的 boot() 方法中。

现在,每当使用 currencies()->setCurrent() 改变当前货币时,例如在以下路由中

Route::get('/currency/change/{currency}', function (string $currency) {
    currencies()->setCurrent($currency);

    return redirect()->back();
});

它也将写入 currency 会话密钥。该路由可以用于您的导航栏中的 <form> 或任何其他 UI 元素。

术语

本节解释了包中使用的术语。

“值”可以指代 Money 对象的多种不同事物。因此,我们使用不同的术语。

基本值

基本值是通过传递给 money() 辅助函数的值。

$money = money(1000);

->value() 方法返回

$money->value(); // 1000

这是货币的实际整数值。在大多数货币中,这将是分。

该软件包使用基础值进行所有货币计算。

小数值

小数值不用于计算,但它可读性好。它通常用于格式化值。

$money = Money::fromDecimal(100.0); // $100 USD
$money->value(); // 10000
$money->decimal(); // 100.0

默认货币中的值

这是将 Money 对象转换为默认货币后的值。

例如,您可能希望管理员可以以任何货币输入产品的价格,但仍以默认货币存储。

通常建议在“代码领域”中使用默认货币。并且仅使用其他货币向用户(例如客户)显示价格或让管理员以对他们有用的货币输入价格。

当然,也有一些例外,有时您可能希望存储货币和项目的值。为此,如果要将整个 Money 对象存储在单个数据库列中,则该软件包具有JSON 编码功能

当然,将整数价格和字符串货币作为单独的列存储也是完全可行的。

格式化值

格式化值是按照其货币规范显示的货币值。它可能使用前缀、后缀、小数分隔符、千位分隔符和显示小数

例如

money(123456, new CZK)->formatted(); // 1 235 Kč

请注意,显示小数可能不同于数学小数

对于捷克克朗(CZK),显示小数将为 0,但数学小数将为 2。这意味着分用于货币计算,而 decimal() 方法将返回基础值除以 100,但显示小数不包括任何分。

原始格式化值

对于上述内容的逆过程,您可以使用 rawFormatted() 方法。此方法返回格式化值,但使用 数学小数作为显示小数。这意味着上述示例中的值将包括分来显示。

money(123456, new CZK)->rawFormatted(); // 1 234,56 Kč

这对于像捷克克朗这样的货币非常有用,这类货币通常不使用分,但 可以 在特定情况下使用分。

货币

当前货币

当前货币指的是当前使用的货币。

默认情况下,该软件包在任何地方都不使用它。所有如 money() 这样的调用都将使用提供的货币或默认货币。

当前货币是在最终计算步骤中将货币转换为用户在浏览器中显示之前可以转换货币的内容。

默认货币

默认货币是在您的代码库上下文中 Money 默认使用的货币。

money() 辅助函数、Money::fromDecimal() 方法以及 new Money() 都使用此货币(除非提供了特定的货币)。

使用默认货币进行数据存储是一个好主意。有关更多信息,请参阅默认货币中的值部分。

数学小数

数学小数指的是在数学环境中货币的小数位数。

所有数学操作仍然在浮点数上使用,使用 基础值,但数学小数用于知道如何在每次操作后对货币进行舍入,如何使用 Money::fromDecimal() 方法实例化它,等等。

显示小数

显示的小数位数指的是在格式化值中使用的位数。

额外功能

Livewire 支持

该软件包开箱即支持 Livewire。您可以将任何 Livewire 属性类型提示为 Money,货币值和货币将存储在组件的状态中。

class EditProduct extends Component
{
    public Money $price;

    // ...
}

Livewire 的自定义类型支持还不是很高级,所以在 Blade 视图中使用起来有点困难——建议使用 Alpine 组件的包装器。在未来版本中,将直接支持 wire:model 用于 currencyvalue

组件大致可以看起来像这样

<div x-data="{
    money: {
        value: {{ $price->decimal() }},
        currency: {{ $price->currency()->code() }},
    },

    init() {
        $watch('money', money => $wire.set('money', {
            value: Math.round(money.value / 100),
            currency: money.currency.
        }))
    },
}" x-init="init">
    Currency: <select x-model="currency">...</select>
    Price: <input x-model="value" type="number" step="0.01">
</div>

JSON 序列化

货币和 Money 实例都可以转换为 JSON,并从 JSON 实例化。

$currency = new CZK;
$json = json_encode($currency);
$currency = Currency::fromJson($json);

$foo = money(100, 'CZK');
$bar = Money::fromJson($money->toJson());
$money->is($bar); // true

提示

💡 接受的货币代码格式

大多数接受货币的方法都接受以下任何一种格式

currency(USD::class);
currency(new USD);
currency('USD');

money(1000, USD::class)->convertTo('CZK');
money(1000, 'USD')->convertTo(new CZK);
money(1000, new USD)->convertTo(CZK::class);

💡 动态添加货币

类货币很优雅,但不是必需的。如果您的货币规格来自数据库或某些 API,您可以注册为数组。

// LoadCurrencies middleware

currencies()->add(cache()->remember('currencies', 3600, function () {
    return UserCurrencies::where('user_id', auth()->id())->get()->toArray();
});

当数据库调用返回一个遵循上述格式的数组数组货币时。

开发和贡献

在本地运行所有检查

./check

代码样式将由 php-cs-fixer 自动修复。

运行测试不需要数据库。