Comment ne pas perdre d'argent à cause des prix flottants : Stockez les prix de la bonne façon
L'argent exige de la précision. Pourtant, de nombreux projets enregistrent encore les prix à l'aide du type flottant, ce qui entraîne inévitablement l'apparition de ces "centimes mystérieux" qui surgissent de nulle part.

Pourquoi les nombres à virgule flottante vous font-ils perdre de l'argent ?
Les types à virgule flottante (float, double) respectent la norme IEEE-754 et stockent les nombres en binaire, et non en décimal. De nombreuses fractions décimales (comme 0.1) ne peuvent pas être représentées exactement en binaire, de sorte que les valeurs sont approximées. C'est pourquoi vous verrez des surprises comme :
echo 0.1 + 0.2; // 0.30000000000000004
Ce n'est pas un bug de PHP - c'est la façon dont les flottants fonctionnent dans presque tous les langages (PHP, JS, Python, Java, C#, Go). Lorsque ces petites erreurs s'accumulent dans les remises, les taxes et les montants des factures, vos livres commencent à dériver par des "centimes mystérieux".
La solution : des types numériques exacts
Arrêtez de stocker l'argent dans des types flottants binaires. Passez à des représentations décimales ou entières exactes qui n'accumulent pas de bruits d'arrondi.
MySQL
- DECIMAL(p, s) - décimal à virgule fixe où les chiffres sont stockés exactement ; idéal pour la comptabilité et les rapports (par exemple,
DECIMAL(10,2)). - BIGINT - stocke les montants en unités mineures (cents, pence).
€12.99devient1299. Rapide, compact, précis. - FLOAT/DOUBLE - à éviter pour les prix ; ils sont approximatifs par conception.
PostgreSQL
- NUMERIC(precision, scale) - décimal exact (DECIMAL de PostgreSQL) ; flexible et précis.
- BIGINT - même approche par unités mineures que MySQL, idéal pour les données transactionnelles.
- MONEY - dépend de la localisation et manque de flexibilité ; non recommandé pour les systèmes multidevises.
Pourquoi BIGINT est-il souvent le meilleur choix ?
BIGINT est un grand type d'entier avec une plage énorme (jusqu'à 9,22×1018). Si vous stockez les prix en centimes, cela représente jusqu'à 92 billions d'euros avec une précision de l'ordre du centime, ce qui est plus que suffisant pour n'importe quelle plateforme de commerce électronique ou SaaS.
- Exactitude par construction : les nombres entiers ne comportent pas de bruit d'arrondi. Ce que vous stockez est ce que vous obtenez, même après des millions d'opérations.
- Performance : l' arithmétique des entiers, les comparaisons, l'indexation, le tri et l'agrégation sont moins coûteux que
DECIMALpour les grands ensembles de données. - Portabilité : les entiers se comportent de manière cohérente dans MySQL, PostgreSQL, SQLite et les ORM. Vous n'avez pas à vous soucier des différences de précision et d'échelle.
- Mathématiques prévisibles : appliquez la TVA/les remises, puis arrondissez une fois aux unités mineures ; pas d'artefacts sur
0.00000001. - Alignement des API : les principaux fournisseurs de paiements (Stripe, Adyen, PayPal) utilisent des unités mineures dans leurs API - les faire correspondre permet d'éviter les problèmes d'arrondi entre les systèmes.
Arithmétique exacte en PHP (évitez aussi les flottants à l'exécution)
Même si la base de données est correcte, votre code peut réintroduire des erreurs de flottants. Utilisez BCMath pour des opérations décimales sûres :
$total = bcadd('0.1', '0.2', 2); // "0.30"
$vat = bcmul('12.99', '0.20', 2); // "2.60"
$gross = bcadd('12.99', '2.60', 2); // "15.59"
bcadd, bcmul, bcdiv fonctionnent sur des chaînes avec une échelle fixe - ainsi "0.1 + 0.2" est toujours égal à "0.3".
Exemple Laravel : minuscule distribution, grande sécurité
Stockez les centimes dans la base de données, exposez "12.99" en PHP. Une distribution minimale effectue la conversion dans les deux sens :
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AsPrice implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (empty($value)) {
return 0;
}
return bcdiv((string) $value, '100', 2);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (empty($value)) {
return 0;
}
return bcmul((string) $value, '100', 0);
}
}
Attachez le cast à votre modèle :
class Product extends \Illuminate\Database\Eloquent\Model
{
protected $casts = [
'price' => \App\Casts\AsPrice::class,
];
}
Schéma avec des devises strictes
Même si vous commencez avec une seule devise, ajoutez une colonne de devise maintenant. Restez strict avec ENUM pour EUR / USD / GBP:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->bigInteger('price'); // stored in minor units (cents)
$table->enum('currency', ['EUR', 'USD', 'GBP'])->default('EUR');
$table->timestamps();
});
Principaux enseignements
- Les flottants sont approximatifs (IEEE-754), et c'est universel à travers les langues - ne les utilisez pas pour l'argent.
- Préférez BIGINT (unités mineures) pour la rapidité, la sécurité et la portabilité ; utilisez DECIMAL/NUMERIC lorsque les rapports exigent des décimales.
- Faites de l'arithmétique avec BCMath en PHP pour que les résultats soient exacts.
- Une petite distribution Laravel rend les prix ergonomiques dans le code tandis que votre base de données reste précise.
- Stockez les devises sous forme d'ENUM (
EUR,USD,GBP) pour éviter les mauvaises données.
Contactez-nous
Un projet en tête?
Indiquez le contexte et l’objectif visé. Nous répondons sous 1 jour ouvrable avec la prochaine étape la plus simple (planning, budget indicatif ou audit rapide).