Symfony货币字段类型和Doctrine

18
您在Doctrine中存储货币价值的策略是什么?Symfony的money字段非常方便,但如何将其映射到Doctrine的列呢?是否有提供DBAL类型的bundle可用于此? 使用float或int列类型是不够的,因为当您处理货币时通常也会涉及货币。我正在使用两个字段来处理此问题,但手动处理很麻烦。

你需要存储整数还是浮点数?我认为你会有一定数量的字段类型限制。 - A.L
@A.L 这没什么关系。我可以随时使用 divisor 选项将整数转换为浮点数。目前我正在使用 int(金额)+ string(货币),但这太尴尬了。 - SiliconMind
你如何使用 + 符号?你有一个字段用于金额和一个字段用于货币吗?如果你需要处理不同的货币,我建议你在问题中添加一条注释,因为这会使问题变得比只有一个货币更加复杂。 - A.L
@A.L + 符号并不重要,因为金额存储为 int,它可以是正值或负值。至少在我的情况下是如此,尽管浮点数也是一样的。 - SiliconMind
4个回答

14

我建议使用像Money\Money这样的值对象。

# app/Resources/Money/doctrine/Money.orm.yml
Money\Money:
  type: embeddable
  fields:
    amount:
      type: integer
  embedded:
    currency:
      class: Money\Currency
# app/Resources/Money/doctrine/Currency.orm.yml
Money\Currency:
  type: embeddable
  fields:
    code:
      type: string
      length: 3
# app/config.yml
doctrine:
  orm:
    mappings:
      Money:
        type: yml
        dir: "%kernel.root_dir%/../app/Resources/Money/doctrine"
        prefix: Money
class YourEntity
{
    /**
     * @ORM\Embedded(class="\Money\Money")
     */
    private $value;

    public function __construct(string $currencyCode)
    {
        $this->value = new \Money\Money(0, new \Money\Currency($currencyCode));
    }

    public function getValue(): \Money\Money
    {
        return $this->value;
    }
}

1
这个解决方案不错,但是仅适用于您不需要可空的嵌入对象,因为不幸的是 Doctrine2 不支持可空的嵌入对象。 - baris1892
不建议将货币存储为整数 https://github.com/Padam87/AccountBundle/issues/5 - 112Legion

13

考虑使用 decimal 类型:

/**
 * @ORM\Column(type="decimal", precision=7, scale=2)
 */
protected $price = 0;
请注意,有一些货币具有三位小数。如果您打算使用这样的货币,则应将scale参数设置为3。如果您打算混合具有两个和三个小数位的货币,则在只有两个小数位的情况下添加一个尾随的0
注意:$price在PHP中将是一个字符串。您可以将其转换为float或将其乘以100(对于具有三位小数的货币,则为1000),然后将其转换为int
货币本身是一个单独的字段;它可以是一个包含三个字母的货币代码的字符串。或者-更清晰的方式-您可以创建一个包含您正在使用的所有货币的表格,然后为货币条目创建一个ManyToOne关系。

8
不要考虑使用“decimal”类型!PHP无法准确表示小数,这对于处理货币来说肯定是一个问题!始终将货币存储在最小单位(分、便士等)中。 - Jonny
1
如果你需要证明这不是一个好主意,可以在你的shell中执行以下命令:echo "<?php var_dump((.1 + .1 + .1 === .3));" | php - Jonny
1
@Jonny: 是的,但只有在直接比较浮点数值时才会出现这个问题。一个专业的程序员会知道他们是要检查绝对或相对 epsilon。此外,Doctrine会将值作为数字字符串提供给您,因此您可以自由地将该字符串乘以1000以获得整数。(是的,在字符串上应用数学运算很混乱,但这就是PHP的特点。) - lxg
这不仅是在比较时的问题,而且是一种表示问题,在进行计算时会导致不准确性。因此,如果您除了显示它之外无法使用该值,为什么不一开始就将其存储为整数呢? - Jonny
2
由于货币价值通常由两个部分组成(例如,美元/分)。第二部分的数字位数可能会有所不同。如果将货币价值存储为整数,则始终需要第二部分数字位数的信息。使用十进制字段,您无需关心数字位数,因为必要时可以进行修剪。 - lxg

4
你可以定义自己的字段类型,只要告诉Doctrine如何处理它。为了说明这一点,我想出了一个“商店”和“订单”,其中使用了一个“货币”-ValueObject。
首先,我们需要一个实体和另一个ValueObject,在实体中使用该对象:

Order.php:

<?php

namespace Shop\Entity;

/**
 * @Entity
 */
class Order
{
    /**
     * @Column(type="money")
     *
     * @var \Shop\ValueObject\Money
     */
    private $money;

    /**
     * ... other variables get defined here
     */

    /**
     * @param \Shop\ValueObject\Money $money
     */
    public function setMoney(\Shop\ValueObject\Money $money)
    {
        $this->money = $money;
    }

    /**
     * @return \Shop\ValueObject\Money
     */
    public function getMoney()
    {
        return $this->money;
    }

    /**
     * ... other getters and setters are coming here ...
     */
}

Money.php:

<?php

namespace Shop\ValueObject;

class Money
{

    /**
     * @param float $value
     * @param string $currency
     */
    public function __construct($value, $currency)
    {
        $this->value  = $value;
        $this->currency = $currency;
    }

    /**
     * @return float
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * @return string
     */
    public function getCurrency()
    {
        return $this->currency;
    }
}

到目前为止没有什么特别的。这里的“魔法”就在于:

MoneyType.php:

<?php

namespace Shop\Types;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use Shop\ValueObject\Money;

class MoneyType extends Type
{
    const MONEY = 'money';

    public function getName()
    {
        return self::MONEY;
    }

    public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return 'MONEY';
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        list($value, $currency) = sscanf($value, 'MONEY(%f %d)');

        return new Money($value, $currency);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof Money) {
            $value = sprintf('MONEY(%F %D)', $value->getValue(), $value->getCurrency());
        }

        return $value;
    }

    public function canRequireSQLConversion()
    {
        return true;
    }

    public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
    {
        return sprintf('AsText(%s)', $sqlExpr);
    }

    public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
    {
        return sprintf('PointFromText(%s)', $sqlExpr);
    }
}

然后您可以使用以下代码:
// preparing everything for example getting the EntityManager...

// Store a Location object
use Shop\Entity\Order;
use Shop\ValueObject\Money;

$order = new Order();

// set whatever needed
$order->setMoney(new Money(99.95, 'EUR'));
// other setters get called here.

$em->persist($order);
$em->flush();
$em->clear();

你可以编写一个映射器,将来自Symfony货币字段的输入映射到Money-ValueObject,以进一步简化此过程。这里解释了更多细节:http://doctrine-orm.readthedocs.org/en/latest/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html 我以前使用过这个概念,并且它有效。如果您有问题,请告诉我。

1
谢谢,这正是我想到的事情。但我会使用“int”来存储最小单位的金额(例如欧元的分)。这种情况的缺点是无法轻松地按货币或金额对表中的数据进行过滤或排序。这对我来说是最大的问题。 - SiliconMind
与 SQL 中的浮点列类型相比,有哪些优势? - A.L
1
当你处理浮点数时,可能会出现对于货币值无效的值,例如0.005。还有舍入问题。处理这些问题的最佳解决方案是采用Fowler's money pattern - SiliconMind
2
你的 MoneyType.php 示例毫无意义。MySQL 中没有 MONEY 函数。你只是复制了 Doctrine 的示例。这是误导性的。 - jkucharovic

2
我正在寻找一个解决此问题的方案,通过谷歌搜索我找到了这个页面:这里
在那里,展示了自Doctrine 2.5起可用的嵌入式字段
使用类似以下的代码,您可以管理像货币这样具有更多“参数”的值:
一个例子:
/** @Entity */
class Order
{
    /** @Id */
    private $id;

    /** @Embedded(class = "Money") */
    private $money;
}

/** @Embeddable */
class Money
{
    /** @Column(type = "int") */ // better than decimal see the mathiasverraes/money documentation
    private $amount;

    /** @Column(type = "string") */
    private $currency;
}

希望这能有所帮助。
更新
我写了一个PHP库,其中包含一些有用的值对象
还有一个值对象来管理货币价值(它包装了伟大的MoneyPHP库),并使用Doctrine类型将其持久化到数据库中。
此类型以100-EUR的形式将值保存到数据库中,表示1欧元。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接