米博/laravel-properties

为 Laravel 价格的 PHP 库

dev-main 2024-06-04 07:46 UTC

This package is not auto-updated.

Last update: 2024-09-26 07:29:12 UTC


README

米博/laravel-properties

codecov

(主要)价格、增值税、货币,还包括其他数量及其单位,如重量、长度、面积、体积、时间、速度、温度等,均包含在此库中。

该库专注于存储、检索、计算、转换、显示以及使用数量及其单位进行其他魔法操作的能力。

目的

从价格及其相关内容开始

  1. 创建一个价格(带或不带增值税);
  2. 创建一个 正价格(一个价格不能是负数);
  3. 计算价格(加法、减法);
  4. 转换价格(例如从 CZK 转换到 EUR);
  5. 根据国家转换 增值税率(检查不同国家的增值税率和其值);
  6. 显示价格(格式化价格到特定区域并显示正确的货币名称或其符号);
  7. 使用不同的货币和/或增值税率计算价格;
  8. 处理 汇率
  9. 使用 历史价格(价格可能是在一段时间前创建的,使用了不同的增值税率和/或值);
  10. 价格以 自定义格式 存入数据库;
  11. 数据库中以自定义格式检索价格;
  12. 根据提供的可定价对象创建 折扣
  13. 检索 组合价格的真实增值税值
  14. 拥有 自定义货币(内部网络/应用程序货币);
  15. 数量计费(例如每 1 公斤、每件等);
  16. 完整的 ISO 货币列表(可以选择使用始终更新的源);

并且不仅仅是价格

  1. 最常见的 数量及其 单位(如长度 - 米、英里、英寸等);
  2. 转换单位(如从米到英里);
  3. SI 前缀单位(例如千、毫);
  4. 公制英制美国 customarySI 单位
  5. 计算数量(加法、减法、乘法除法);
  6. 导出数量(如速度、加速度等);
  7. 使用打印机显示数量;
  8. 以自定义格式将数量存入和检索数据库;
  9. 创建一个数量(指定或默认单位);
  10. 访问 十进制 属性(可以选择数字的小数位数);
  11. 访问更精确的 浮点 属性(在考虑速度和内存的同时优化计算);
  12. 能够 创建自定义数量、单位或安装额外的单位;
  13. 支持自定义 非数值 数量(如衣服尺寸 - inDev);
  14. 比较属性(如是否相等、大于、小于、在之间等);

以及更多,适用于 Laravel 框架。注意,这些功能中的大多数都可以在此库外部访问,并可用于任何其他 PHP 项目。此库仅将它们收集在一起,并为 Laravel 和 Eloquent 提供了额外的功能,例如数据库的属性转换、翻译等。

安装

composer require mibo/laravel-properties

php artisan vendor:publish --provider="MiBo\Properties\Providers\ConfigProvider"

首先应编辑配置文件 prices.php 并添加一个要使用的增值税解析器和转换器。没有它们,价格只能为零。

要使用比较价格、舍入、向上取整或向下取整,应安装 \MiBo\Prices\Contracts\PriceCalculatorHelper 的实现以进行舍入,以及/或 \MiBo\Prices\Contracts\PriceComparer 以比较价格,并在配置文件中设置它们。

用法

创建价格

为了避免使用许多类来创建一个价格,我们创建了一个工厂,它可以用来创建具有任何所需属性的价格。含增值税或不含增值税?没问题。具体的增值税率?货币?这些都是可能的。
默认情况下,工厂使用配置文件中的默认值。每次在工厂上调用 ::get() 方法都会将默认值恢复,以便用户创建一个新的价格,同时只保留一个工厂实例。

$factory = \MiBo\Prices\Data\Factories\PriceFactory::get();

$factory->setValue(10) // The price value
    ->setCurrency('USD') // The currency that the price should have
    ->setDate(\Carbon\Carbon::now()->addYears(-1)) // The price might have been created a time ago
    ->setIsVATIncluded(true)
    ->create();

上面的示例将返回一个总价值为含增值税10美元的价格 => 不含增值税少于10美元,而下面的示例将返回一个不含增值税总价值为10美元的价格 => 含增值税超过10美元。

$factory->setValue(10)
    ->setCurrency('USD')
    ->setDate(\Carbon\Carbon::now()->addYears(-1))
    ->setIsVATIncluded(false)
    ->create();

可以创建一个只包含默认值(货币、增值税率、当前时间)的空价格。

$factory->create();

可以指定价格不得为负数。当您想创建一个或计算一个通常不应为负数的价格时,这可能会很有用,例如购买的价格。如果此类价格的价值为负,则被视为无效,并抛出错误。如果基于增值税的价格中的至少一个为负,则价格被视为负。

$factory->setValue(10)
    ->strictlyPositive()
    ->create();

计算价格

由于价格本身具有直接的方法,添加和减去价格很容易,不需要使用任何额外的类来计算它们。

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->add($price); // Adding another or the same instance of the Price
$price->add(10); // Adding a number of int or float
$price->subtract($price); // Subtracting another or the same instance of the Price
$price->subtract(10.0); // Subtracting a number of int or float

如果提供int或float,则认为值与当前价格具有相同的单位(货币)、增值税率和日期,而值不含增值税。用户必须注意,将float或int添加或减去到增值税率结合的价格会触发错误,因为在那种情况下没有关于预期添加或减去的价格类型的信息。

不同的货币?
当计算具有不同货币的多个价格时,将使用汇兑器将价格转换为相同的货币。在这种情况下,当前价格保持在其货币中,而给定主题转换为当前价格的货币。
这种计算价格的方式为我们提供了更多的灵活性,无需在计算之前将每个价格都转换为货币。汇兑器仅在需要时使用。

不同的增值税率?
同样,当计算时,我们只想组合兼容的主题。因此,会检查两个价格具有相同的增值税率。首先,将增值税国家更改为当前价格的国家。然后,比较增值税率。如果增值税率不同,则将当前价格的增值税率设置为'COMBINED',这代表两个或更多增值税率的组合。这种解决方案使我们能够计算最终价格的真正价值,同时仍然允许我们将价格转换为不同国家的货币或增值税率。

需要更多计算?
除了添加和减去之外,价格还提供乘法和除法。当然,价格乘以价格没有意义,但可以使用以下方式来执行乘法:

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->multiply(10); // Multiplying by int or float

或者需要一个每单位的价格?例如,10欧元每1毫米可以做到!还可以进行其他转换!

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->divide(\MiBo\Properties\Length::MILLI(1));

转换

为了确保我们提供正确的货币的最终价格,可以直接在价格上调用方法

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->convertToUnit(\MiBo\Prices\Units\Price\Currency::get('EUR'));

该方法本身负责更改价格的货币并使用汇兑器转换价格的价值。

国际销售

每个国家都有自己的增值税率,具有不同的类别和增值税率的价值。为了确保用户完全避免检查是否正确计算价格,价格对象提供了一个方法来简单地更改增值税国家。然后,getPriceWithVAT 返回给定国家的增值税价格。就像这样

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->forCountry('SVK');

打印机和区域

一些国家有独特的显示价格的方式(没有冒犯之意)。为确保价格正确显示,Price 对象附带了可以在配置中设置的打印机。有一个选项可以根据请求使用原生 PHP 格式,但并不总是最佳选择。

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->print(); // $10.00 / 10,00 € / or set up yours just like you want it!

打印机可以直接在价格上调用,也可以作为一个独立的对象使用,并将价格解析到其中。打印机提供指定小数位数的能力。库中包含所有当前货币的符号和货币名称。

默认情况下,价格有 PricePrinter,它会根据请求的本地化自动更改显示的货币(可在价格的本地化文件中自定义)。可以通过将打印机的公共静态属性 $convertCurrencyByLocale 设置为 false 来忽略此行为。
此外,打印机使用 Laravel 的 Auth 门面检索当前用户,如果用户是 \MiBo\Properties\Contracts\SubjectToTax 接口的实例,则打印机将使用用户的信息,无论其是否为增值税纳税人。如果用户未登录,打印机将使用配置文件中设置的默认值。如果用户是增值税纳税人,则显示的价格不含增值税,否则含增值税。

历史价格

对于一年前或更早的购买,增值税率可能已经改变。不仅增值税值,甚至其税率和其他可能的内容。为此,DateTime 被用于价格,并且始终在检查价格的真实值时使用。如果没有指定 DateTime,则认为价格是当前的。
简单来说,如果面包的增值税在 2010 年为 0%,现在为 20%,且不含增值税的价格始终为 10 欧元,那么指定年份 2010 的价格将返回含增值税的 10 欧元,而没有指定时间或 2010 年及以后的时间将返回含增值税的 22 欧元。

Eloquent

天哪!价格如此复杂,包含如此多的信息,可能会使数据库中的列变得混乱。我们中的许多人不需要存储关于价格的所有信息,因为我们可能不想使用超过一种货币。我们只想使用当前的价格。想使用较小的单位而不是较大的单位,以便在数据库中无需使用浮点数或小数。这是(我希望)对每个人的解决方案!请知会一声,如果不符合的话。

\MiBo\Prices\Data\Casting\PriceAttribute 加入到这个活动中,并可根据每个人的需求进行配置。

让我们考虑一个模型 MyModel 的 tbl_product 表。

class MyModel extends \Illuminate\Database\Eloquent\Model
{
    protected $table = 'tbl_product';

    protected $casts = [
        'price' => \MiBo\Prices\Data\Casting\PriceAttribute::class,
    ];
}

属性转换器在模型在应用程序中使用时将模型的数据转换为 \MiBo\Prices\Price 对象,并将所有价格信息存储到数据库中,因此开发者不需要关心所有这些信息。默认情况下,使用增值税类别 ''、当前日期、配置中的货币和配置中的国家创建/存储不含增值税的价格。但是

附加列

如果表中包含附加列,转换器将使用它们的值来创建价格对象,并将价格信息存储在这些列中。为了继续上面的例子,让我们在迁移中添加新的列

function(\Illuminate\Database\Schema\Blueprint $table) {
    $table->string('price_currency', 3); // Currency code of the price
    $table->string('price_country', 3); // Country code of the price
    $table->string('price_category', 8); // VAT category (classification) of the price
    $table->date('price_date'); // Date of the price
}

现在,转换器将使用这些列。列的 'price' 前缀取决于属性名称。

不同的列名(后缀)
有时我们确实想重命名列(例如,将 'price_cat' 替换为 'price_category')。这可以通过在转换器中指定列名来完成

    protected $casts = [
        'price' => \MiBo\Prices\Data\Casting\PriceAttribute::class . ':category-_cat',
    ];

在这个例子中,我们告诉转换器对于类别,应使用后缀 _cat

不同的列名(完整名称)
日期、货币和国家可能具有完全不同的列名,应使用这些列名。这样做的主要原因是一个产品或订单的价格可能使用创建时间而不是自己的列,这会导致信息重复。注意,在这种情况下,转换器不会更改列中的信息,而是在存储之前更改或转换价格。以下是一个使用常见created_at列作为价格日期的示例

    protected $casts = [
        'price' => \MiBo\Prices\Data\Casting\PriceAttribute::class . ':date-created_at',
    ];

模型的固定值

有时我们确实知道特定模型的货币总是相同的。转换器允许我们在设置转换器的地方直接为模型定义货币、日期、国家等

    protected $casts = [
        'price' => \MiBo\Prices\Data\Casting\PriceAttribute::class . ':currency-EUR,country-SVK',
    ];

在上面的示例中,我们告诉转换器所有价格都存储在欧元中,用于增值税的国家是斯洛伐克。转换器将使用这些值来创建正确提供的数据。在存储价格时,转换器会确保正确存储值,并在需要时进行转换。

转换器的设置

设置位于转换器类名的列(:)之后,其中键和值通过破折号(-)分隔。对使用逗号(,)分隔。转换器提供了以下设置

  • 货币currency
    • 如果没有提供且列不存在,则使用配置中的值;
    • 如果没有提供,但存在带有后缀 _currency 的列,则使用该值创建价格;
    • 如果提供了以 _ 开头的字符串,则使用带有该后缀的列创建价格;
    • 如果提供了不以 _ 开头的字符串且存在具有该名称的列,则使用该值;
    • 如果提供的字符串包含有效的 ISO 4217 货币代码,则使用该值(例如 EUR)。
  • 正值positive
    • 如果没有提供,则创建可以负的价格;
    • 如果指定,则价格将创建为正或负(truefalse);
    • 使用 positive-true 仅创建正价格。
  • 类别category
    • 如果在转换器上设置了 setCategoryCallback() 方法,则使用该闭包来获取类别;
    • 如果没有指定且存在带有后缀 _category 的列,则使用该列;
    • 如果指定了以 _ 开头的字符串且存在具有该后缀的列,则使用该值;
    • 如果指定了字符串,则使用该字符串作为类别;
    • 如果没有有效的值,则使用 null 或空字符串。
  • 国家country
    • 如果没有指定且存在带有后缀 _country 的列,则使用该值;
    • 如果指定了带有 _ 的字符串且存在具有该后缀的列,则使用该值;
    • 如果指定了字符串,则使用该字符串作为国家(期望 ISO 3166-1 alpha-3/2);
    • 如果没有有效的值,则使用配置中的值。
  • 日期date
    • 如果没有指定且存在带有后缀 _date 的列,则使用该值;
    • 如果指定了带有 _ 的字符串且存在具有该后缀的列,则使用该值(读写);
    • 如果指定了且存在列,则使用该列(只读);
    • 如果指定了格式为 Y-m-d,则将其用作日期;
    • 如果没有有效的值,则使用当前日期。
  • 任何增值税率any
    • 如果没有指定,则使用增值税解析器作为结果的增值税率;
    • 如果设置为 'true',则将任何增值税率设置为结果。
  • 含增值税vat
    • 如果没有指定,则存储和创建不含增值税的值;
    • 如果设置为 'true',则存储和创建含增值税的值。
  • 小数单位inMinor
    • 如果没有指定,则将值存储在该货币的小数单位中(EUR中的分) - 允许使用整数而不是浮点数或小数。
    • 如果设置为'false',该值将存储在该货币的主要单位中(EUR中的欧元)。

折扣

折扣及其应用可能有些棘手。当应用折扣时,应该使用哪种增值税率?如何检查哪些可以折扣,哪些不能?

我们提供了一个工厂,该工厂根据设置和提供的可折扣对象创建折扣。

在工厂上使用'setOption'可以设置百分比折扣或固定金额折扣,含或不含增值税,仅针对指定增值税率或任何,以及更多。

$factory  = \MiBo\Prices\Data\Factories\DiscountFactory::get();
$discount = $factory->setOption(\MiBo\Prices\Data\Factories\DiscountFactory::OPT_VALUE, 550)
    ->setOption(\MiBo\Prices\Data\Factories\DiscountFactory::OPT_SUBJECT, [$productList])
    ->create();

工厂提供以下选项

  • OPT_COUNTRY用于指定折扣的国家;
  • OPT_FILTER用于设置可折扣对象列表的过滤器;
  • OPT_IS_VALUE_WITH_VAT想使用含增值税的值或不含/你想对含增值税或不含增值税的价格应用折扣吗?
  • OPT_PERCENTAGE_VALUE使用百分比吗?指定其值(0-100)
  • OPT_REQUIRES_WHOLE_SUM_TO_USE想确保折扣只有在完全使用时才应用吗?
  • OPT_SUBJECT什么将被折扣?提供可以折扣的对象列表;
  • OPT_TYPE百分比或固定金额的类型?或自定义?创建自己的折扣类型;
  • OPT_VALUE可用的折扣值(例如,100 CZK);
  • OPT_VAT折扣的增值税率。可用于任何东西?还是仅适用于指定税率的商品?

使用

\MiBo\Prices\Data\Factories\DiscountFactory::customType(
    'your-type-name',
    function (
        iterable $subject,
        \MiBo\Prices\PositivePrice|\MiBo\Prices\PositivePriceWithVAT $discount,
        array $config
    ): \MiBo\Prices\PositivePrice|\MiBo\Prices\PositivePriceWithVAT {
       // your code
    }
);
\MiBo\Prices\Data\Factories\DiscountFactory::get()
    ->setOption(\MiBo\Prices\Data\Factories\DiscountFactory::OPT_TYPE, 'your-type-name')
    ->create();

货币

默认情况下,使用ISO货币列表加载器。它加载包含所有当前货币的列表。但是,您可以创建、实现或使用不同的加载器。为什么?创建自己的货币以供您的应用程序使用,作为对用户的好处。更新交换机以将您的货币转换为另一种货币并设置其汇率。

比较

此功能需要实现某些接口并在配置文件中进行配置。尽量避免获取价格值并将其与另一个值进行比较。特别是当比较两个浮点值时,结果可能出乎意料。使用通用的价格接口。它提供了所有必需的方法及其否定,因此您可以在不获取其值的情况下比较两个价格。

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->isBetween(10, $price);
$price->isLessThan($price);
$price->isGreaterThanOrEqualTo($price);
$price->isZero();
$price->isNegative();
$price->is($price);
$price->hasSameValueAs($price);
$price->hasNotSameValueWithVATAs($price);

您主要可以使用价格对象或浮点数/整数。并且,您可以四舍五入、向上取整和向下取整值。对于这些中的每一个,您都可以指定精度(是的,对于向下取整和向上取整也是!)

/** @var \MiBo\Prices\Price $price */
$price = {...};
$price->ceil(2);
$price->floor(-2);
$price->round(2, PHP_ROUND_HALF_DOWN);

翻译

大多数(或所有)来自库mibo/properties的属性和价格属性的属性在这个库中都有翻译。
注意: 翻译没有SemVer化,任何现有翻译的修复,即使它已更改,也被认为是补丁。新的翻译(对于新区域设置和/或新属性)被认为是次要更改。

每个属性都有单独的翻译文件,该文件通过属性数量的翻译名称调用,该名称在getNameForTranslation()方法中指定。常见的返回值包括'name'键,它是属性的翻译名称,'units'键,它是可用的单位列表。每个单位都以其代码/符号为键。值包含一个'name'(单位的翻译名称,用于整数值),'name-float'(单位的翻译名称,用于浮点值——一些语言对于整数和浮点值有不同的名称),符号(单位的符号)。在属性的翻译中有一个'format'键,它包含用于格式化属性值的格式——'short'和'long'。格式用于,因为一些语言对于例如价格($10 for en_US; 10 $ for cs_CZ)有不同的格式。注意: 摄氏度的'短格式'被忽略并且(应该)始终结果为1°C(没有空格)。