我们将所有与货币相关的值存储在我们的数据库中(ODM,但ORM可能表现相同).我们使用MoneyType将面向用户的值(12,34€)转换为他们的美分表示(1234c).这里出现了
typical float precision问题:由于精度不足,很多情况会产生舍入错误,这些错误仅在调试时可见. MoneyType会将传入的字符串转换为可能不精确的浮点数(“1765”=> 1764.9999999998).
一旦你坚持这些价值观,事情就会变得糟糕:
class Price { /** * @var int * @MongoDB\Field(type="int") **/ protected $cents; }
将转换传入的值(浮动!),如:
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,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 Forms / Doctrine而言)? NumberTransformer或MoneyType不应该返回整数值 – 人们可能也想保存浮点数,所以我们无法解决那里的问题.我考虑过覆盖持久层中的IntType,有效地舍入每个传入的整数值而不是转换.另一种方法是将字段存储为MongoDB中的float …
基本的PHP问题is discussed here.
现在我决定使用我自己的MoneyType,在内部对整数调用“round”.
原文链接:https://www.f2er.com/php/134240.html<?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'],$options['divisor'] )) ; } }