Jak obsługiwać wartości pieniężne w PHP i MySql?

16

Odziedziczyłem ogromny stos starszego kodu napisanego w PHP na bazie danych MySQL. Zauważyłem, że aplikacja używa doublesdo przechowywania danych i manipulowania nimi.

Teraz natknąłem się na wiele postów mówiących o tym, że doublenie nadają się do operacji pieniężnych z powodu błędów zaokrąglania. Jednak jeszcze nie znalazłem kompletnego rozwiązania, w jaki sposób wartości pieniężne powinny być obsługiwane w kodzie PHP i przechowywane w bazie danych MySQL.

Czy istnieje najlepsza praktyka, jeśli chodzi o obchodzenie się z pieniędzmi w PHP?

Czego szukam to:

  1. Jak dane powinny być przechowywane w bazie danych? typ kolumny? rozmiar?
  2. Jak powinny postępować dane przy normalnym dodawaniu, odejmowaniu. mnożenie czy dzielenie?
  3. Kiedy powinienem zaokrąglać wartości? Ile zaokrągleń jest dopuszczalne, jeśli w ogóle?
  4. Czy istnieje różnica między obsługą dużych wartości pieniężnych a niskich?

Uwaga: bardzo uproszczone przykładowy kod jak mogę napotkać wartości pieniądza w życiu codziennym (różne obawy związane z bezpieczeństwem były ignorowane uproszczenie oczywiście w prawdziwym życiu nigdy nie używać mojego kodu takiego.):

$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?

Mam nadzieję, że wykorzystasz ten przykładowy kod jako sposób na wydobycie większej liczby przypadków użycia i nie potraktowanie go dosłownie.

Pytanie bonusowe Jeśli mam używać ORM, takich jak Doctrine lub PROPEL, jak różne będą pieniądze w moim kodzie.

Songo
źródło
1
Nie znam PHP, ale znajomość terminologii jest nieoceniona w tych scenariuszach dotyczących google-fu. Termin, którego szukasz, to „dowolna precyzja”, na którą Google reaguje za pomocą php.net/manual/en/book.bc.php
Jimmy Hoffa
1
@Jimmy Hoffa: precyzja arbitralna na ogół nie jest tym, czego potrzebujesz. Oczywiście często oba są łączone, ale np. decimalTyp w C # ma ograniczoną precyzję, ale doskonale nadaje się do wartości pieniężnych.
Michael Borgwardt
1
@MichaelBorgwardt racja, zapomniałem o typach racjonalnych, ponieważ tak wiele języków ich nie ma. Dobra decyzja.
Jimmy Hoffa
Mając dużo pracy z walutami i starszym kodem, powiedziałbym, że idź do przechowywania go w liczbach całkowitych i używania centów zamiast dolarów. Uważaj jednak, 32 liczby całkowite mogą pomieścić zaskakująco małą liczbę. 4) 32-bitowa liczba całkowita może pomieścić tylko 21 milionów, jeśli nie jest podpisana i używa centów.
Pieter B
Nawiasem mówiąc, twój przykładowy kod, który pokazuje ceny i upusty, które są publikowane, przeraża mnie. Mamy nadzieję, że jest tak uproszczony, że nie odzwierciedla rzeczywistego użycia, ale na pierwszy rzut oka wygląda na to, że ufasz przeglądarce, która powie ci, jaka jest cena przedmiotu, POST zwracając ukryte pole lub coś takiego.
Carson63000,

Odpowiedzi:

6

Obsługa liczb w PHP / MySQL może być dość trudna. Jeśli użyjesz liczby dziesiętnej (10,2), a liczba będzie dłuższa lub ma wyższą precyzję, zostanie ona obcięta bez błędów (chyba że ustawisz odpowiedni tryb dla serwera db).

Aby obsługiwać duże wartości lub wartości o wysokiej precyzji, możesz użyć biblioteki takiej jak BCMath , która pozwoli ci wykonać podstawowe operacje na dużych liczbach i zachować wymaganą precyzję.

Nie jestem pewien, jakie dokładnie obliczenia wykonasz, ale musisz również pamiętać, że (0,22 * 0,4576) + (0,78 * 0,4576) nie będzie równe 0,4576, jeśli nie zastosujesz odpowiedniej precyzji w tym procesie.

Maksymalny rozmiar DECIMAL w MySQL to 65, więc powinno być więcej niż wystarczające do dowolnego celu. Jeśli użyjesz pola typu DECIMAL, zostanie ono zwrócone jako ciąg znaków bez względu na użycie ORM lub zwykłego PDO / mysql (i).

Jak dane powinny być przechowywane w bazie danych? typ kolumny? rozmiar?

DECIMAL z precyzją, której potrzebujesz. Jeśli korzystasz z kursów wymiany, potrzebujesz co najmniej czterech miejsc po przecinku

Jak powinny postępować dane przy normalnym dodawaniu, odejmowaniu. mnożenie czy dzielenie?

Użyj BCMatha, aby być po stronie zapisywania i dlaczego użycie float może nie być dobrym pomysłem

Kiedy powinienem zaokrąglać wartości? Ile zaokrągleń jest dopuszczalne, jeśli w ogóle?

W przypadku wartości pieniężnych dopuszczalne są normalnie dwa miejsca po przecinku, ale możesz potrzebować więcej, jeśli na przykład korzystasz z kursów walut.

Czy istnieje różnica między obsługą dużych wartości pieniężnych a niskich?

Zależy, co rozumiesz przez duże. Jest zdecydowanie różnica między obsługą liczb z wysoką precyzją.

onlineapplab.com
źródło
9

Prostym obejściem jest przechowywanie ich jako liczb całkowitych. 99,99 przechowywane jako 9999. Jeśli to nie zadziała (i istnieje wiele powodów, dla których może to być zły wybór), możesz użyć typu Dziesiętny. http://dev.mysql.com/doc/refman/5.0/en/precision-math-decimal-changes.html po stronie mysql. Po stronie php znalazłem ten /programming/3244094/decimal-type-in-php, który może być tym, czego szukasz.

Pytanie dodatkowe: Trudno powiedzieć. Orm będzie działał w oparciu o wybrane typy danych. Powiedziałbym, że możesz zrobić coś z abstrakcją, aby pomóc, ale ten konkretny problem nie jest rozwiązany po prostu poprzez przejście do ORM.

Ominus
źródło
1
FWIW, Drupal Commerce wykorzystuje sztuczkę 9999 do przechowywania swoich cen.
Florian Margaine
1
„i istnieje wiele powodów, dla których może to być zły wybór” Czy mógłbyś podzielić się niektórymi problemami związanymi z tą praktyką?
Songo,
2
@Songo: patrz floating-point-gui.de/formats/integer
Michael Borgwardt
@MichaelBorgwardt Dziękuję za informację, Songo przepraszam za opóźnienie, huragan nas wyniósł :)
Ominus
3

Postaram się umieścić w tym moje doświadczenie:

Czego szukam to:

Jak dane powinny być przechowywane w bazie danych? typ kolumny? rozmiar?

Korzystałem DECIMAL(10,2)z mysql bez problemów (8 ukończeń i 2 miejsca po przecinku == 99,999.999,99 == ogromna ilość), ale to zależy od zakresu pieniędzy, które musisz pokryć. Ogromne ilości należy brać ze szczególną ostrożnością (na przykład wartości maksymalne OS). W części dziesiętnej używam 2 wartości, aby uniknąć obcięcia lub zaokrąglenia. Biorąc pod uwagę pieniądze, jest kilka przypadków, w których potrzebujesz więcej miejsc po przecinku (w takim przypadku musisz upewnić się, że użytkownik będzie pracował z nimi wszystkimi, w przeciwnym razie dane będą bezużyteczne)

Jak powinny postępować dane przy normalnym dodawaniu, odejmowaniu. mnożenie czy dzielenie?

Pracuj z jedną walutą i tabelą wymiany (z datami). W ten sposób masz pewność, że zawsze będziesz mieć poprawną kwotę zapisaną. Dodatkowo: zapisz pełne wartości i utwórz widok z wynikami obliczeń. Pomoże to ustalić wartości w locie

Kiedy powinienem zaokrąglać wartości? Ile zaokrągleń jest dopuszczalne, jeśli w ogóle?

Znowu zależy to od zasięgu pieniężnego systemu. Zawsze myśl w kategoriach KISS, chyba że musisz wpaść w chaos wymiany walut

Czy istnieje różnica między obsługą dużych wartości pieniężnych a niskich?

W zależności od systemu operacyjnego i języków programowania zawsze musisz sprawdzić wartości maksymalne i minimalne

Alwin Kesler
źródło
Dziękuję za odpowiedź, ale jeśli będę musiał poradzić sobie z czymś takim $valueToBeStored= $a * $b;, jak $ai $boba są odczytywane jako dziesiętne z bazy danych, myślę, że zostaną one przeniesione doublew PHP, prawda? czy to wpłynie na liczby?
Songo
$ai $bsą pobierane z db? więc w moim przykładzie nie musisz nigdy przechowywać, $valueToBeStoredponieważ zawsze będziesz mieć źródło $ai $bdane. Możesz więc programowo pracować z wartością w funkcji lub tak dalej lub utworzyć widok mysql z wynikiem kolumny. W ten sposób, jeśli jakakolwiek wartość musi zostać zmieniona, nie musisz się martwić modyfikowaniem wielu miejsc (podatnych na błędy)
Alwin Kesler,