0,1 + 0,2 ne fait pas 0,3 en PHP : le résultat exact renvoyé par l’interpréteur est 0,30000000000000004. Ce n’est pas un bug, c’est la conséquence de la représentation binaire des nombres à virgule flottante, qui ne peut pas coder exactement la plupart des fractions décimales. Pour un article de blog, l’anecdote fait sourire. Pour une extension WooCommerce qui recalcule des remises, des marges ou des frais additionnels, elle produit des écarts de centime qui finissent en ticket de support.
La solution employée par la quasi-totalité des bibliothèques de paiement sérieuses consiste à ne jamais manipuler de flottants pour les montants d’argent : on travaille en entiers, exprimés dans la plus petite unité de la devise (le centime pour l’euro), et on ne convertit en décimal qu’à l’affichage final.
Pourquoi le flottant trahit votre calcul
Un flottant IEEE 754 double précision stocke une approximation binaire. Additionner plusieurs prix, appliquer un pourcentage de remise puis arrondir peut décaler le résultat d’un centime par rapport à ce qu’attend le client, en particulier quand les opérations s’enchaînent avant l’arrondi final. WooCommerce lui-même stocke les montants en chaînes de caractères formatées avec un nombre de décimales fixe pour limiter le problème côté cœur, mais dès qu’une extension personnalisée fait ses propres additions en flottant avant de repasser par wc_format_decimal(), l’erreur peut ressurgir.
Construire une classe Money minimale

L’idée : un objet immuable qui encapsule un entier de centimes et un code devise, avec des méthodes d’addition, de multiplication et de conversion. Aucune opération arithmétique n’est faite en dehors de cette classe.
final class Money {
private int $cents;
private string $currency;
public function __construct(int $cents, string $currency = 'EUR') {
$this->cents = $cents;
$this->currency = $currency;
}
public static function fromDecimal(string $amount, string $currency = 'EUR'): self {
return new self((int) round(((float) $amount) * 100), $currency);
}
public function add(Money $other): self {
$this->assertSameCurrency($other);
return new self($this->cents + $other->cents, $this->currency);
}
public function multiply(float $factor): self {
return new self((int) round($this->cents * $factor), $this->currency);
}
public function toDecimal(): string {
return number_format($this->cents / 100, 2, '.', '');
}
private function assertSameCurrency(Money $other): void {
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException('Devises incompatibles');
}
}
}
Chaque opération renvoie une nouvelle instance : l’objet reste immuable, ce qui évite qu’une remise appliquée deux fois par erreur ne contamine silencieusement un panier. La conversion en décimal, via toDecimal(), n’intervient que lorsqu’il faut afficher le prix ou le transmettre à une méthode WooCommerce qui attend une chaîne.
Brancher la classe sur les hooks de prix WooCommerce
Pour une extension qui ajoute une remise personnalisée sur le panier, on récupère le sous-total via WC()->cart->get_subtotal(), on le convertit immédiatement en Money::fromDecimal(), on effectue le calcul, puis on repasse en décimal avant d’appeler add_fee() :
- Convertir chaque montant entrant en
Moneydès sa réception, jamais plus tard - Faire circuler des objets
Moneyentre les fonctions internes de l’extension - Ne reconvertir en chaîne décimale qu’au point de sortie, vers WooCommerce ou l’affichage
Les pièges qui restent malgré la classe
Le premier piège est l’arrondi lui-même : multiplier des centimes par un facteur de remise produit parfois un résultat à mi-chemin entre deux entiers. La fonction round() de PHP utilise par défaut l’arrondi à l’entier le plus proche, avec un comportement spécifique sur les valeurs à exactement 0,5 ; il vaut mieux fixer explicitement le mode via le second argument si votre politique métier l’exige.
Le second piège concerne les devises à zéro décimale, comme le yen japonais, où « centime » n’a pas de sens : la classe doit rester paramétrable sur le nombre de décimales par devise plutôt que de figer 100 en dur partout.
En résumé
Passer par une classe Money en entiers ne change pas la logique métier de votre extension de tarification, seulement la représentation interne des montants. Le coût de mise en place est faible face au gain : plus d’écart de centime imprévisible dans les rapports comptables, et un code de calcul de prix qui devient trivial à tester unitairement, puisque les entiers se comparent avec une égalité stricte, contrairement aux flottants.