在 Laravel/PHP 中处理资金:基本技巧

202 阅读5分钟

管理 Laravel 项目中的财务,例如设置产品价格或最终确定发票,至关重要。本指南提供了用于精确计算的最佳实践和工具。

在 Laravel 中处理资金:避免浮动

请考虑以下数据库列定义:

$table->decimal('price', 8, 2);  
// Where 8 represents the total digits and 2 signifies decimal digits

要在 Laravel Blade 模板中显示价格,您可以编写:


Total price: ${{ number_format($product->price, 2) }}  
// This ensures values like 9.1 appear as 9.10

这种方法似乎很简单,并且可能适用于使用单一货币的项目,而无需复杂的计算。但有一个隐藏的陷阱:舍入问题。由于编程语言和数据库处理浮点数的方式,错误可能会悄然出现,有时会导致小至 0.01 的差异。虽然看起来可以忽略不计,但这可能会随着时间的推移或更大的交易而累积。 避免在数据库中将货币值存储为浮点数。 舍入错误的风险虽然很低,但不值得冒这个风险。探索下面概述的更安全的替代方案。

整数:Laravel 中更聪明的赚钱方法

有没有想过将货币价值存储为整数,或者更具体地说,存储为美分?

与其保留 1.23 之类的浮点值,不如考虑将其保存为整数:123。乍一听可能很不合常规,因为我们通常以美元和美分来考虑,而不仅仅是美分。但这种方法提供了更好的精度。

值得庆幸的是,Laravel 可以使用 Model Attributes: Accessors 和 Mutators 无缝地处理这种转换。

以下是您的设置方法:


class Product extends Model  
{  
protected function price(): Attribute  
{  
return Attribute::make(  
get: fn ($value) => $value / 100,  
set: fn ($value) => $value * 100  
);  
}  
}

对于那些仍在使用旧的 Laravel 语法的人:

class Product extends Model  
{  
protected function getPriceAttribute($value)  
{  
return $value / 100;  
}  
protected function setPriceAttribute($value)  
{  
$this->attributes['price'] = $value * 100;  
}  
}

因此,如果用户输入 123.45,则会在数据库中保存为 12345。当您显示它时,它会转换回熟悉的 123.45 格式。

但是,需要考虑一些细微差别: 不同的十进制用法: 并非每种货币都像美元一样使用两位小数。有些有 3-4 位小数,例如突尼斯第纳尔或巴林第纳尔。在这种情况下,请调整您的计算,分别乘以或除以 1000 或 10,000。

无小数: 有些货币根本没有小数。对于这些,只需保存值而不进行任何转换。

精度要求: 有时,您可能需要更高的精度,存储 0.0123 USD 等值。如果是这样,请在数据库中将其保存为 123,以允许所需的最大十进制精度。

但请记住,核心原则仍然是:支持整数以实现更一致和精确的货币价值处理。

在 Laravel 中使用 PHP 包进行资金管理

金钱不仅仅是数字。除了数字之外,它还与货币和汇率有关。对于涉及多种货币的项目,管理计算可能会变得复杂。

这种复杂性是价值对象数据传输对象旨在解决的问题。简而言之,这些通过将资金转化为具有属性的对象来提供帮助,使其更易于管理。

下面是一个使用 MoneyPHP 包的图示:

use Money\Currency;  
use Money\Money;  
$fiver = new Money(500, new Currency('USD'));
$value1 = Money::EUR(800); // €8.00  
$value2 = Money::EUR(500); // €5.00  
$value3 = Money::EUR(600); // €6.00  
$result = $value1->add($value2, $value3); // €19.00

为此,两个值得注意的软件包是:

moneyphp/money

brick/money

后者 brick/money 的工作原理是这样的:

use Brick\Money\Money;  
$money = Money::of(50, 'USD');  
echo $money->plus('4.99'); // USD 54.99

它很酷的功能之一:

$money = Money::of(100, 'USD');  
[$a, $b, $c] = $money->split(3); // Outputs: USD 33.34, USD 33.33, USD 33.33

这两个软件包都提供相似的核心功能:它们将货币价值转换为多功能对象。

在数据库方面,您的方法仍然相似:您将金额存储为整数。对于涉及不同货币的项目,您还需要保存货币代码。

您的数据库迁移可能如下所示:


Schema::create('orders', function (Blueprint $table) {  
$table->id();  
$table->integer('price');  
$table->string('currency')->default('USD');  
$table->timestamps();  
});

假设您在数据库中有一个价格为 7907(或 79.07 美元)且货币为 USD 的订单。您的控制器可能如下所示:


public function show(Order $order) {  
return view('orders.show', [  
'id' => $order->id,  
'price' => Money::ofMinor($order->price, $order->currency)->formatTo('en_US'),  
]);  
}

在 Blade 视图中:

Order ID: {{ $id }} ({{ $price }})

这将输出:“Order ID: 1 ($79.07)”。

使用 Laravel 的 Custom Casts 增强资金管理 Laravel 有一个内置功能 Custom Casts,可简化资金管理。这样,你就不需要重复使用 Money::ofMinor() 之类的函数。$order 的 price 属性可以直接转换为 Money 对象。

首先,您将生成一个 Custom Cast:

php artisan make:cast Money

这将创建 app/Casts/Money.php。像这样配置它:


class Money implements CastsAttributes {  
public function get($model, string $key, $value, array $attributes) {  
return \Brick\Money\Money::ofMinor($attributes['price'], $attributes['currency']);  
}  
public function set($model, string $key, $value, array $attributes) {  
if (!$value instanceof \Brick\Money\Money) {  
return $value;  
}  
return $value->getMinorAmount()->toInt();  
}  
}

然后,将此类链接到您的模型:

use App\Casts\Money;  
class Order extends Model {  
protected $casts = [  
'price' => Money::class  
];  
}

现在,您的 Controller 可以简化:

class OrderController extends Controller {  
public function show(Order $order) {  
return view('orders.show', compact('order'));  
}  
}
Order ID: {{ $order->id }}  
<br />  
Price: {{ $order->price->formatTo('en_US') }}

使用这种方法,$order->price 自动成为 Money 对象。您可以直接使用它的方法。 这类似于 Laravel 对 created_at 和 updated_at 时间戳的处理,它们是 Carbon 对象。例如,在 Blade 中, {{ $order->created_at->diffForHumans() }} 可以无缝工作。 自定义强制转换有两种方法:get() 和 set()。虽然 get() 很简单,但 set() 方法需要小心。根据您传递给 price 字段的内容,处理方式会有所不同。如果它只是一个整数,则返回值。如果它是一个 Money 对象,则需要进行 like $value->getMinorAmount()->toInt() 的转换。

在处理国际交易时,货币兑换是必不可少的。虽然货币兑换可能会变得复杂,但 Laravel 的 brick/money 套餐简化了流程。 以下是使用 brick/money 包处理转换的方法:

此包附带一个使用汇率提供程序的类 Brick\Money\CurrencyConverter

对于一个简单的情况,让我们使用 ConfigurableProvider,它允许我们手动设置汇率。

在我们的模型中,我们创建一个将价格转换为欧元的方法:

use Brick\Math\RoundingMode;  
use Brick\Money\CurrencyConverter;  
use Brick\Money\ExchangeRateProvider\ConfigurableProvider;  
class Order extends Model {  
public function getPriceEurAttribute() {  
$exchangeRateProvider = new ConfigurableProvider();  
$exchangeRateProvider->setExchangeRate('USD', 'EUR', '0.9123');  
$converter = new CurrencyConverter($exchangeRateProvider);  
return $converter->convert(  
moneyContainer: $this->price,  
currency: 'EUR',  
roundingMode: RoundingMode::DOWN  
);  
}  
}

在您的视图中,您现在可以同时显示原始价格和转换后的价格:

Price: {{ $order->price->formatTo('en_US') }} (Converted: {{ $order->price_eur->formatTo('en_US') }})

输出将是: Price: $57.15 (Converted: €52.13)

除了基本的 ConfigurableProvider 之外,该软件包还提供:

PDOProvider:从数据库中检索汇率。

BaseCurrencyProvider:当您的所有汇率与单一基础货币进行比较时,此功能非常有用。

您还可以通过实现 ExchangeRateProvider 接口来灵活地设计自定义提供程序。

此外,对于最新的汇率,您可以从外部 API 中提取数据或使用数据集(例如来自欧洲中央银行的数据集),确保您的汇率始终是最新的。

在 Laravel 中处理货币值时,您有特定的定制包,比一般的 PHP 包更简化您的工作。在这里,让我们讨论一下 akaunting/laravel-money,一个用于 Laravel 应用程序的便捷工具。

akaunting/laravel-money 不仅仅是以前 PHP 包的包装器,而是为 Laravel 设计的独立解决方案。

Key Features: 主要特点:

Money 对象创建: 使用各种输入方法轻松创建 Money 对象。

Money::USD(500); // Using Money class  
money(500, 'USD') // Using a helper function

Blade 指令: 它为干净的模板提供了简单的 Blade 指令。

@money(500, 'USD')

叶片组件: 将组件引入 Blade 模板以获得简洁的语法。

<x-money amount="500" currency="USD" />  
<x-currency currency="USD" />

还有其他有价值的包装包,如 cknow/laravel-money。它简化了上述 MoneyPHP 包的使用,同时添加了 Laravel 特定的功能,例如:

自定义演员表: 允许在 Eloquent 模型中直接使用金钱对象,而无需手动转换。

助手: 提供方便的帮助程序函数来简化您的编码过程。

Blade 指令: 支持在 Blade 模板中轻松插入货币值。

使用这些包,您可以轻松地在 Laravel 项目中处理、格式化和转换货币,而无需处理与编程中的货币操作相关的通常复杂性。