10

I keep a field in the database that has a decimal type.

How can I type this?

public function getPrice() : decimal
{
    $temp = 123.45;
    return $temp;
}

It is not working, because return:

Fatal error: Uncaught TypeError: Return value of getPrice() must be an instance of decimal, float returned.

Anyway type decimal not exists.

I can use float for typping, but is this safe and good? The point is that with the float type there can be numerical errors (Float or decimal for prices?).

jilo
  • 103
  • 1
  • 1
  • 5

3 Answers3

20

Don't listen to recommendation to use floats for money values. It's really dangerous, because floating numbers cannot guarantee the accurate comparison of non-integer values.

Good solution is to use integers with multipliers. The best solution is to create ValueObject which represents type money.

Alex
  • 4,621
  • 1
  • 20
  • 30
3

I would recommend you use string both when reading and when writing DECIMAL values, to minimize the temptation to do "simple" arithmetic within PHP using them. As soon as you cast them to a float and do a computation, you risk losing the precision. Leave those computations within the DB itself, and use strings for input/output.

EDIT: If you REALLY have to do computations in PHP, convert the strings to integers in cents, do all of your computations in cents, and turn them back to a decimal string afterwards.

Here's two helper functions I use at work:

class MathHelper
{

/**
 * Reliably divide an integer by (10 ^ $precision) without extensions.
 *
 * @param int|string $amountInCents The amount in cents
 * @param int $precision The precision of $amountInCents. 2 by default,
 * as this function should most often be used for currencies where 1 whole = 100 cents.
 *
 * @return string The amount with $precision digits after the decimal place.
 * For example, if $amountInCents is 1, and $precision is 2, the result will be 0.01.
 * If $amountInCents is 1, and $precision is 3, the result will be 0.001.
 *
 * Intended for use in a DECIMAL database column.
 */
public static function centsToWhole($amountInCents, int $precision = 2): string
{
    return ($amountInCents < 0 ? '-' : '') .
        substr_replace(
            str_pad(abs($amountInCents), $precision + 1, '0', STR_PAD_LEFT),
            '.',
            -$precision,
            0
        );
}

/**
 * Convert a whole amount to cents.
 *
 * Opposite of {@link MathHelper::centsToWhole()}.
 *
 * @param string $amount The whole amount.
 * @param int $precision Expect the given whole amount to have this precision. 2 by default,
 * as this function should most often be used for currencies where 1 whole = 100 cents.
 *
 * @return int|string The amount as cents, where "10 ** $precision" cents are required to form one whole.
 * If the value can't fit in a PHP integer, it will be returned as a string (suitable for use in unsigned BIGINT DB columns).
 */
public static function wholeToCents(string $amount, int $precision = 2)
{
    $parts = explode('.', $amount, 2);
    $whole = $parts[0];
    $cents = $parts[1] ?? '';
    if (strlen($cents) > $precision) {
        throw new ArithmeticError(
            'The given amount has higher precision than the given one. Round the amount in advance if data loss is acceptable'
        );
    }
    if ($precision === 0) {
        return (int)$whole;
    }

    $total = ltrim($whole, '0') . str_pad($cents, $precision, '0', STR_PAD_RIGHT);
    return PHP_INT_MIN <= $total && $total <= PHP_INT_MAX
        ? (int)$total
        : $total;
}
}

You'd fetch DECIMAL from DB, call MathHelper::wholeToCents(), compute what needs to be computed, and turn the result back to whole with MathHelper::centsToWhole().

Note that as written, the above functions expect the input to be valid. Don't use them on unvalidated user input, or you'll get malformed output, which may in turn lead to all sorts of problems.

boen_robot
  • 1,450
  • 12
  • 24
  • inability to do calculations within the application code itself is an awful, awfully limiting constraint – chili Sep 24 '21 at 00:08
  • 2
    @chili Fair enough. I've added functions I use at work to convert between DECIMAL and int where needed. Still though, it's best to not even use those, and do computations in SQL wherever possible. We have these functions at work because it's an old system, originally written by people who didn't know better. – boen_robot Sep 24 '21 at 11:29
  • @chili Welcome in the world of PHP. If you can, avoid this language. – Martin Braun Apr 27 '22 at 19:17
-15

Don't worry ... Float is fine and is safe.

If your prices are less than ten decimal places long, you're fine.

However:

Never trust floating number results to the last digit, and do not compare floating point numbers directly for equality.

Floating point numbers have limited precision. Although it depends on the system, PHP typically uses the IEEE 754 double precision format, which will give a maximum relative error due to rounding in the order of 1.11e-16. Non elementary arithmetic operations may give larger errors, and, of course, error propagation must be considered when several operations are compounded.

Additionally, rational numbers that are exactly representable as floating point numbers in base 10, like 0.1 or 0.7, do not have an exact representation as floating point numbers in base 2, which is used internally, no matter the size of the mantissa. Hence, they cannot be converted into their internal binary counterparts without a small loss of precision. This can lead to confusing results: for example, floor((0.1+0.7)*10) will usually return 7 instead of the expected 8, since the internal representation will be something like 7.9999999999999991118....

Adi219
  • 4,712
  • 2
  • 20
  • 43
  • 26
    Floats are not fine for money – Ed Heal Aug 23 '17 at 19:29
  • It also matters the number of decimal places to the other side of the decimal point. – Ed Heal Aug 23 '17 at 21:38
  • Most of this text is copied from the php.net docs is accurate, but the statement "If your prices are less than ten decimal places long, you're fine." is inaccurate. Depending on your precision settings, stuff like 19.99 will be represented as 19.899999999 which can break expectations in other parts of your code or APIs you are using. – strangerstudios Mar 21 '22 at 20:24
  • Why nobody has mentioned that if you store value as decimal in the DB, and then you fetch it and use it as "string" in PHP, you can do calculations using the strings without loosing precision. Any problems with that? Try this in PHP: "1.123456" * "1.123456" – John Smith Aug 16 '22 at 01:54
  • @JohnSmith even if you do that, PHP is converting the strings to floats to do the calculations. For example, 0.2 * 3 = 0.6, but in PHP `0.2 * 3 == 0.6` is false (because it's really 0.6000000000000000882...) and that happens even if you write them as strings `'0.2' * '3' == '0.6'` – Tobia May 30 '23 at 16:15