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.30000000000000004Ce 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 intégrations 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".
BCMath garantit la précision de vos calculs. Pour d'autres bonnes pratiques PHP, consultez Un ensemble minimal de pratiques PHP pour un code lisible.
Exemple Laravel : petit cast, grande sécurité
Stockez les centimes dans la base de données, exposez "12.99" en PHP. Un cast minimal 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 dans tous les langages - 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.
- Un petit cast 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.
Est-il sûr d'utiliser float pour l'argent dans une base de données ?
Non. Les types à virgule flottante (float, double) suivent la norme IEEE-754, qui stocke les nombres en binaire. Les fractions décimales comme 0.1 ne peuvent pas être représentées exactement, ce qui provoque de minuscules erreurs d'arrondi. Celles-ci s'accumulent dans les remises, les taxes et les factures — produisant finalement des différences de centimes inexpliquées. Utilisez plutôt DECIMAL ou BIGINT.
Comment Stripe et les autres fournisseurs de paiement stockent-ils les montants ?
Stripe, Adyen et PayPal utilisent tous des unités mineures — des entiers représentant la plus petite unité monétaire. Ainsi, 12,99 € est envoyé comme 1299, 50,00 $ comme 5000. Si votre base de données stocke également les prix en unités mineures (BIGINT), vous évitez les erreurs d'arrondi à la frontière de l'intégration. Pas de float, pas de dérive.
DECIMAL ou BIGINT — lequel est le mieux pour stocker les prix ?
Les deux sont exacts et sûrs pour l'argent. BIGINT stocke les prix en unités mineures (centimes), ce qui est plus rapide pour les calculs, l'indexation et le tri — et correspond au format utilisé par Stripe, Adyen et PayPal dans leurs API. DECIMAL stocke des valeurs lisibles (12,99) directement, ce qui peut être pratique pour les rapports SQL. Pour la plupart des applications web et plateformes e-commerce, BIGINT est le choix pragmatique par défaut.
Comment éviter les erreurs de flottants dans les calculs PHP ?
Utilisez l'extension BCMath. Les fonctions comme bcadd(), bcmul() et bcdiv() opèrent sur des représentations en chaînes avec une échelle décimale fixe. Ainsi, 0,1 + 0,2 est toujours égal à 0,3 — sans surprises IEEE-754. N'utilisez jamais les opérateurs PHP natifs (+, *, /) pour les montants monétaires.
Contactez-nous
Un projeten 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).