symfony2 coniarytype与除数:整数转换导致错误的数据库值



我们将所有与资金相关的价值存储为数据库中(ODM,但ORM可能的行为可能相同)。我们正在使用MoneyType将面对价值(12,34欧元)的用户转换为他们的美分表示(1234C)。典型的浮点精度问题在这里出现:由于精度不足,有许多情况会产生舍入错误,这些错误在调试时仅可见。MoneyType会将传入的字符串转换为 floats 可能不精确的字符串(" 1765" => 1764.99999999998)。

一旦您坚持这些价值观,事情就会变得不好:

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

将转换传入值( float !),例如:

namespace DoctrineODMMongoDBTypes;
class IntType extends Type
{
    public function convertToDatabaseValue($value)
    {
        return $value !== null ? (integer) $value : null;
    }
}

(整数)铸件将剥离值的曼蒂萨(Mantissa),而不是四舍五入,从而有效地导致将错误的值写入数据库(1764而不是1765年,而" 1765"是内部1764.999999999998)。

这是一个单元测试,应该从任何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表格/学说而言)是什么?NumberStransformer或MoneyType不应该返回整数值 - 人们可能还想保存浮子,因此我们无法在此处解决该问题。我考虑过在持久性层中覆盖inttype,从而有效地将每个传入的整数值而不是铸造。另一种方法是将字段存储在MongoDB中...

这里讨论了基本的PHP问题。

现在,我决定使用自己的货币类型,在内部呼叫"圆形"。

<?php
namespace AcmeBundleForm;
use SymfonyComponentFormFormBuilderInterface;

class MoneyToLocalizedStringTransformer extends SymfonyComponentFormExtensionCoreDataTransformerMoneyToLocalizedStringTransformer {
    public function reverseTransform($value)
    {
        return round(parent::reverseTransform($value));
    }
}
class MoneyType extends SymfonyComponentFormExtensionCoreTypeMoneyType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->addViewTransformer(new MoneyToLocalizedStringTransformer(
                $options['scale'],
                $options['grouping'],
                null,
                $options['divisor']
            ))
        ;
    }
}

在我看来,这个问题与持久性层更相关,我会尝试通过覆盖ODMint类型来解决它:

AppBundleDoctrineTypesMyIntType

use DoctrineODMMongoDBTypesIntType;
class MyIntType extends IntType
{
    public function convertToDatabaseValue($value)
    {
        return $value !== null ? round($value) : null;
    }
}

app/config/config.yml

doctrine:
    dbal:
        types:
            int:  AppBundleDoctrineTypesMyIntType

最新更新