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.