我们将所有与货币相关的值存储为美分(在ODM中,但ORM的行为可能相同)。我们正在使用MoneyType将面向用户的值(12,34€)转换为其美分表示形式(1234c)。此处出现typical float precision问题:由于精度不足,在很多情况下会产生舍入错误,这些错误仅在调试时可见。 MoneyType会将传入的字符串转换为,使 float ,可能不精确(“1765” => 1764.9999999998)。

只要坚持以下这些值(value)观,事情就会变糟:

class Price {
    /**
     * @var int
     * @MongoDB\Field(type="int")
     **/
    protected $cents;
}

将像下面这样转换传入的值(是float!):
namespace Doctrine\ODM\MongoDB\Types;
class IntType extends Type
{
    public function convertToDatabaseValue($value)
    {
        return $value !== null ? (integer) $value : null;
    }
}

(整数)强制类型转换将去除值的尾数而不是舍入该值,从而有效地导致将错误的值写入数据库中(当内部的“1765”为1764.9999999998时,是1764而不是1765)。

这是一个单元测试,应该从任何Symfony2容器中显示问题:
//for better debugging: set ini_set('precision', 17);

class PrecisionTest extends WebTestCase
{
    private function buildForm() {
        $builder = $this->getContainer()->get('form.factory')->createBuilder(FormType::class, null, []);
        $form = $builder->add('money', MoneyType::class, [
            'divisor' => 100
        ])->getForm();
        return $form;
    }

    // high-level symptom
    public function testMoneyType() {
        $form = $this->buildForm();
        $form->submit(['money' => '12,34']);

        $data = $form->getData();

        $this->assertEquals(1234, $data['money']);
        $this->assertEquals(1234, (int)$data['money']);

        $form = $this->buildForm();
        $form->submit(['money' => '17,65']);

        $data = $form->getData();

        $this->assertEquals(1765, $data['money']);
        $this->assertEquals(1765, (int)$data['money']); //fails: data[money] === 1764
    }

    //root cause
    public function testParsedIntegerPrecision() {

        $string = "17,65";
        $transformer = new MoneyToLocalizedStringTransformer(2, false,null, 100);
        $value = $transformer->reverseTransform($string);

        $int = (integer) $value;
        $float = (float) $value;

        $this->assertEquals(1765, (float)$float);
        $this->assertEquals(1765, $int); //fails: $int === 1764
    }

}

请注意,此问题并不总是可见的!如您所见,“12,34”运行良好,“17,65”或“18,65”将失败。

解决此问题的最佳方法是什么(就Symfony形式/原则而言)? NumberTransformer或MoneyType不应返回整数值-人们可能还想保存浮点数,因此我们无法在那里解决问题。我考虑过在持久层中重写IntType,有效地舍入每个传入的整数值,而不是强制转换。另一种方法是将字段以float形式存储在MongoDB中...

基本的PHP问题is discussed here

最佳答案

现在,我决定使用自己的MoneyType,该类型在内部对整数进行“舍入”。

<?php

namespace AcmeBundle\Form;

use Symfony\Component\Form\FormBuilderInterface;


class MoneyToLocalizedStringTransformer extends \Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer {

    public function reverseTransform($value)
    {
        return round(parent::reverseTransform($value));
    }
}

class MoneyType extends \Symfony\Component\Form\Extension\Core\Type\MoneyType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->addViewTransformer(new MoneyToLocalizedStringTransformer(
                $options['scale'],
                $options['grouping'],
                null,
                $options['divisor']
            ))
        ;
    }
}

07-25 21:38