2016-12-22 9 views
7

私たちのお金に関連するすべての値は、データベースにセントとして格納されています(ODMではORMは同じように動作します)。 MoneyTypeを使用して、ユーザー向きの値(12,34€)をセントの表現(1234c)に変換しています。 typical float precision問題が発生します。精度が不十分なため、デバッグ時に表示されるだけの丸め誤差を生成するケースが多いです。 MoneyTypeは、受信した文字列をfloatに変換しますが、正確ではない場合があります( "1765" => 1764.9999999998)。symfony2除数を持つMoneyType:整数変換によりデータベースの値が間違っています

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のときは1765ではなく1764)。この問題は、常に表示されていないことを、

//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 
    } 

}

注:

は、ここで任意のSymfony2のコンテナ内から問題が表示されるはずですユニットテストです! 「12,34」がうまくいっているのを見ると、「17,65」または「18,65」は失敗します。

ここで(Symfony Forms/Doctrineに関して)回避する最も良い方法は何ですか? NumberTransformerまたはMoneyTypeは整数値を返すものではありません。浮動小数点を保存して問題を解決できない場合もあります。私は永続化層のIntTypeをオーバーライドすることを考えました。キャストの代わりにすべての着信整数値を丸めます。もう一つのアプローチは、MongoDBにfloatとしてフィールドを格納することです。

基本的なPHPの問題is discussed here

+0

浮動小数点数値を 'NUMERIC(precision、scale)'として保存する 'DecimalType'は、元の精度に影響を与えずに文字列として復帰します。 – yceruto

+0

ええ、私はそう思った。しかし、私は明らかに "浮動小数点値"(1765.00)を "int"として格納するのがとても奇妙です。 – Stefan

+0

100除数( "17.65")なしで伝えたい – yceruto

答えて

2

今のところ、私は内部的に整数の "round"を呼び出す自分の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'] 
      )) 
     ; 
    } 
} 
0

この問題は、永続化層に、より関連していると私はODMさんintタイプオーバーライドすることで、それを解決しようとするだろう:

AppBundle\Doctrine\Types\MyIntType

use Doctrine\ODM\MongoDB\Types\IntType; 

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

app/config/config.yml

doctrine: 
    dbal: 
     types: 
      int: AppBundle\Doctrine\Types\MyIntType 
+0

私はこれが原因ではなく問題の結果を修正するので、私はかなり同意しません。実体は価格をセントで保存していると言っていますが、それは12.97で終わり、実際の生活の可能性を反映するものではないため、0.97cを持つことはできません。私の意見では、Symfonyの変圧器で@Stefanが答えた答えが – malarzm

+0

@Andrey Mischenko:それは私の最初のアプローチ(ODMのみ)でしたが、私はmalarzmの議論について考えました。あなたの提案は確かに問題を解決するでしょう。しかし、私はすべての*入ってくる整数が "round"を渡さなければならないという考えが嫌いです。 – Stefan

関連する問題