Ste*_*fan 7 php mongodb symfony doctrine-orm
我们将所有与货币相关的值存储在我们的数据库中(ODM,但ORM可能会表现相同).我们使用MoneyType将面向用户的值(12,34€)转换为他们的美分表示(1234c).在典型的float精度问题就出现在这里:由于精确度不够有创造调试时仅仅是可见的舍入误差许多情况下.MoneyType会将传入的字符串转换为可能不精确的浮点数("1765"=> 1764.9999999998).
一旦你坚持这些价值观,事情就会变得糟糕:
class Price {
/**
* @var int
* @MongoDB\Field(type="int")
**/
protected $cents;
}
Run Code Online (Sandbox Code Playgroud)
将转换传入的值(浮动!),如:
namespace Doctrine\ODM\MongoDB\Types;
class IntType extends Type
{
public function convertToDatabaseValue($value)
{
return $value !== null ? (integer) $value : null;
}
}
Run Code Online (Sandbox Code Playgroud)
(整数)强制转换将剥离值的尾数而不是舍入值,从而有效地导致将错误的值写入数据库(当"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
}
Run Code Online (Sandbox Code Playgroud)
}
请注意,此问题并不总是可见!你可以看到"12,34"运行良好,"17,65"或"18,65"将失败.
在这里解决的最佳方法是什么(就Symfony Forms/Doctrine而言)?NumberTransformer或MoneyType不应该返回整数值 - 人们可能也想保存浮点数,所以我们无法解决那里的问题.我考虑过覆盖持久层中的IntType,有效地舍入每个传入的整数值而不是转换.另一种方法是将字段存储为MongoDB中的float ...
这里讨论基本的PHP问题.
现在我决定使用我自己的 MoneyType,它在内部对整数调用“round”。
<?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']
))
;
}
}
Run Code Online (Sandbox Code Playgroud)