管理 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 项目中处理、格式化和转换货币,而无需处理与编程中的货币操作相关的通常复杂性。