Como lidar com valores monetários em PHP e MySql?

16

Eu herdei uma pilha enorme de código legado escrito em PHP em cima de um banco de dados MySQL. O que notei é que o aplicativo usa doublespara armazenamento e manipulação de dados.

Agora me deparei com vários posts mencionando como doublenão são adequados para operações monetárias por causa dos erros de arredondamento. No entanto, ainda não encontrei uma solução completa sobre como os valores monetários devem ser tratados no código PHP e armazenados em um banco de dados MySQL.

Existe uma prática recomendada quando se trata de lidar com dinheiro especificamente em PHP?

O que estou procurando são:

  1. Como os dados devem ser armazenados no banco de dados? tipo de coluna? Tamanho?
  2. Como os dados devem ser tratados em adição normal, subtração. multiplicação ou divisão?
  3. Quando devo arredondar os valores? Quanto arredondamento é aceitável, se houver?
  4. Existe uma diferença entre lidar com valores monetários grandes e valores baixos?

Nota: Um exemplo de código MUITO simplificado de como posso encontrar valores monetários na vida cotidiana (várias preocupações de segurança foram ignoradas para simplificação. É claro que na vida real eu nunca usaria meu código como este):

$a= $_POST['price_in_dollars']; //-->(ex: 25.06) will be read as a string should it be cast to double?
$b= $_POST['discount_rate'];//-->(ex: 0.35) value will always be less than 1
$valueToBeStored= $a * $b; //--> any hint here is welcomed 

$valueFromDatabase= $row['price']; //--> price column in database could be double, decimal,...etc.

$priceToPrint=$valueFromDatabase * 0.25; //again cast needed or not?

Espero que você use esse código de exemplo como um meio de trazer mais casos de uso e não interpretá-lo literalmente, é claro.

Pergunta sobre bônus Se eu devo usar um ORM como Doctrine ou PROPEL, quão diferente será usar dinheiro no meu código.

Songo
fonte
1
Não sei PHP, mas saber que a terminologia é inestimável nesses cenários para o google-fu, o termo que você procura é "precisão arbitrária", à qual o google responde com php.net/manual/en/book.bc.php
Jimmy Hoffa
1
@ Jimmy Hoffa: a precisão arbitrária geralmente não é o que você precisa, algo que pode representar e trabalhar com precisão com frações decimais. Obviamente, você encontra frequentemente os dois combinados, mas, por exemplo, o decimaltipo em C # tem precisão limitada, mas é perfeitamente adequado para valores monetários.
Michael Borgwardt
1
@ MichaelBorgwardt certo, esqueço os tipos racionais porque muitas línguas não os têm. Boa decisão.
Jimmy Hoffa
Tendo trabalhado muito com moedas e código legado, eu diria que o armazene em números inteiros e use centavos em vez de dólares. Cuidado, porém, 32 números inteiros podem conter uma quantidade surpreendentemente pequena.4) um número inteiro de 32 bits só pode armazenar uma quantidade de 21 milhões se não estiver assinado e usando centavos.
Pieter B
Como um aparte, seu código de exemplo que mostra preços e descontos sendo postados me aterroriza. Espero que seja tão simplificado que não reflita o uso real, mas, pelo valor de face, parece que você está confiando no navegador para lhe dizer qual é o preço de um item postando de volta um campo oculto ou algo assim.
Carson63000

Respostas:

6

Pode ser bastante complicado manipular números com PHP / MySQL. Se você usar decimal (10,2) e seu número for maior ou tiver maior precisão, ele será truncado sem erros (a menos que você defina o modo adequado para o servidor db).

Para manipular valores grandes ou valores de alta precisão, você pode usar uma biblioteca como o BCMath , permitindo que você faça operações básicas em grandes números e mantenha a precisão necessária.

Não tenho certeza do que exatamente você fará os cálculos, mas lembre-se de que (0,22 * 0,4576) + (0,78 * 0,4576) não será igual a 0,4576 se você não usar a precisão adequada durante o processo.

O tamanho máximo de DECIMAL no MySQL é 65, portanto deve ser mais que suficiente para qualquer finalidade. Se você usar o tipo de campo DECIMAL, ele será retornado como string, independentemente do uso de um ORM ou simplesmente do PDO / mysql (i).

Como os dados devem ser armazenados no banco de dados? tipo de coluna? Tamanho?

DECIMAL com precisão que você precisa. Se você estiver usando taxas de câmbio, precisará de pelo menos quatro casas decimais

Como os dados devem ser tratados em adição normal, subtração. multiplicação ou divisão?

Use BCMath para salvar e por que usar float pode não ser uma boa ideia

Quando devo arredondar os valores? Quanto arredondamento é aceitável, se houver?

Para valores monetários normais, duas casas decimais são aceitáveis, mas você pode precisar de mais se, por exemplo, estiver usando taxas de câmbio.

Existe uma diferença entre lidar com valores monetários grandes e valores baixos?

Depende do que você quer dizer com grande. Definitivamente, há uma diferença entre lidar com números com alta precisão.

onlineapplab.com
fonte
9

Uma solução simples é armazená-los como números inteiros. 99,99 armazenado como 9999. Se isso não funcionar (e há muitos motivos pelos quais isso pode ser uma má escolha), você pode usar o tipo Decimal. http://dev.mysql.com/doc/refman/5.0/en/precision-math-decimal-changes.html no lado do mysql. No lado do php, encontrei este /programming/3244094/decimal-type-in-php que pode ser o que você procura.

Pergunta bônus: Difícil de dizer. O orm funcionará com base nos tipos de dados escolhidos. Eu diria que você poderia fazer algumas coisas com abstração para ajudar, mas esse problema específico não é resolvido simplesmente mudando para um ORM.

Ominus
fonte
1
FWIW, o Drupal Commerce usa o truque 9999 para armazenar seus preços.
Florian Margaine
1
"e há muitas razões pelas quais essa pode ser uma má escolha" Você poderia compartilhar alguns dos problemas dessa prática?
Songo
2
@Songo: veja floating-point-gui.de/formats/integer
Michael Borgwardt
@MichaelBorgwardt Obrigado pela informação, Songo desculpas pelo atraso, furacão nos levou para fora :)
ominus
3

Vou tentar colocar minha experiência nisso:

O que estou procurando são:

Como os dados devem ser armazenados no banco de dados? tipo de coluna? Tamanho?

Eu tenho usado DECIMAL(10,2)para o mysql sem problemas (8 completos e 2 decimais == 99.999.999,99 == grande quantidade), mas isso depende do intervalo de dinheiro que você precisará cobrir. Quantidades enormes devem ser tomadas com cuidado extra (valores máximos de flutuação do OS, por exemplo). Na parte decimal, uso 2 valores para evitar valores truncados ou arredondados. Tomando dinheiro, há poucos casos em que você precisa de mais casas decimais (nesse caso, você precisa garantir que o usuário trabalhe com todos eles, caso contrário, são dados inúteis)

Como os dados devem ser tratados em adição normal, subtração. multiplicação ou divisão?

Trabalhe com uma moeda e uma tabela de câmbio (com datas). Dessa forma, você garante que sempre terá a quantidade correta salva. Um extra: salve valores completos e crie uma visualização com os resultados do cálculo. Isso ajudará você a fixar valores rapidamente

Quando devo arredondar os valores? Quanto arredondamento é aceitável, se houver?

Novamente, depende do intervalo de dinheiro do seu sistema. Sempre pense nos termos do KISS, a menos que precise cair na bagunça da casa de câmbio

Existe uma diferença entre lidar com valores monetários grandes e valores baixos?

Dependendo do sistema operacional e das linguagens de programação, você sempre precisa verificar os valores máximo e mínimo

Alwin Kesler
fonte
Obrigado pela resposta, mas se eu tiver que lidar com algo como $valueToBeStored= $a * $b;se $ae $bforem lidos como decimais no banco de dados, acho que eles serão convertidos doubleno PHP, certo? isso afetará os números?
Songo
$ae $bsão retirados de db? portanto, no meu exemplo, você nunca precisa armazenar $valueToBeStoredporque sempre terá a fonte $ae os $bdados. Então você pode trabalhar programaticamente o valor em uma função ou mais ou criar uma visualização mysql com o resultado da coluna. Dessa forma, se qualquer valor tem que ser mudado você não precisa se preocupar sobre como modificar vários lugares (sujeito a erros)
Alwin Kesler