Le WordPress d'aujourd'hui, décodé pour les développeurs

Extensions

Un objet valeur pour un prix TTC plutôt qu’un flottant en boutique

Stocker un prix en centimes dans un objet dédié évite les erreurs d'arrondi qui s'accumulent avec des opérations flottantes répétées sur une boutique.

Par WordPress Développement • 29 octobre 2022 • 5 min de lecture • Aucun commentaire
Un objet valeur pour un prix TTC plutôt qu'un flottant en boutique

1,10 € multiplié par trois ne donne pas exactement 3,30 € en arithmétique flottante binaire. Le résultat réel avoisine 3,2999999999999998, et selon l’endroit où l’on arrondit dans le code, la facture affichée peut différer d’un centime de celle enregistrée en base. Sur une boutique qui traite quelques dizaines de commandes par jour, l’écart reste invisible. Sur un catalogue avec des remises en cascade, des frais de port variables et une TVA appliquée à plusieurs étapes du panier, il finit par apparaître dans les rapprochements comptables.

La cause n’est pas un bug ponctuel mais un choix de représentation : un float PHP est une approximation binaire d’un nombre décimal, jamais une valeur exacte pour la plupart des montants en euros. La correction ne consiste pas à multiplier les round() un peu partout dans le code, mais à changer la structure qui porte le prix.

Le problème posé par le flottant

Dans une extension de boutique construite au fil des évolutions, il n’est pas rare de trouver des prix stockés tantôt en float, tantôt en chaîne de caractères formatée, avec des additions effectuées directement sur ces valeurs à plusieurs endroits : dans le calcul du sous-total, dans l’application d’une remise, dans le calcul de la TVA, puis à nouveau lors de l’export vers un logiciel de facturation. Chaque opération introduit une infime imprécision, et ces imprécisions s’additionnent au lieu de s’annuler.

Le symptôme le plus visible est un total de commande qui diffère d’un centime entre l’affichage du panier et le récapitulatif de la page de paiement, ou entre le montant enregistré en base et celui renvoyé par un point d’entrée REST personnalisé.

Un objet valeur en centimes entiers

L'essentiel à retenir : Centimes en entier plutôt qu'euros en flottant ; Un objet immuable qui porte ses propres opérations ; Comparaisons fiables entre deux prix

La solution la plus robuste consiste à ne jamais manipuler de flottant pour un montant : on stocke un entier représentant des centimes, encapsulé dans une petite classe qui porte ses propres opérations. Cet objet valeur devient la seule porte d’entrée pour additionner, multiplier ou comparer des prix.

final class Montant
{
    private function __construct(private readonly int $centimes)
    {
    }

    public static function depuisCentimes(int $centimes): self
    {
        return new self($centimes);
    }

    public static function depuisEuros(float $euros): self
    {
        return new self((int) round($euros * 100));
    }

    public function ajouter(Montant $autre): self
    {
        return new self($this->centimes + $autre->centimes);
    }

    public function multiplier(int $quantite): self
    {
        return new self($this->centimes * $quantite);
    }

    public function appliquerTaux(float $taux): self
    {
        return new self((int) round($this->centimes * $taux));
    }

    public function estSuperieurA(Montant $autre): bool
    {
        return $this->centimes > $autre->centimes;
    }

    public function versEuros(): string
    {
        return number_format($this->centimes / 100, 2, ',', ' ');
    }
}

Le constructeur privé oblige à passer par une méthode nommée explicite : on ne crée jamais un Montant à partir d’un flottant sans passer par un arrondi contrôlé, effectué une seule fois, au moment de la conversion depuis une source externe (formulaire, import CSV, réponse d’une API de paiement).

Où placer la conversion

Le point sensible n’est pas l’objet lui-même mais la frontière entre le monde flottant (affichage, saisie utilisateur) et le monde entier (calcul, stockage). Trois règles suffisent en pratique :

  • Convertir en centimes entiers dès la lecture d’une valeur saisie ou importée, jamais plus tard dans le flux.
  • Ne jamais reconvertir en euros pour effectuer un calcul intermédiaire, seulement pour l’affichage final.
  • Stocker en base une colonne entière (INT UNSIGNED ou BIGINT selon les montants attendus), jamais une colonne DECIMAL convertie en flottant côté PHP à la lecture.

Pour une commande avec trois lignes de produits, une remise en pourcentage et une TVA à taux réduit, l’enchaînement devient : addition des sous-totaux en Montant, application de la remise via appliquerTaux(), calcul de la TVA sur le montant après remise, puis affichage à la toute fin via versEuros().

Variantes selon le contexte

Pour une boutique multi-devises, l’objet valeur gagne à porter aussi un code devise (EUR, USD) et à refuser toute opération entre deux instances de devises différentes, avec une exception explicite plutôt qu’une conversion silencieuse. Pour un contexte purement national, cette complexité n’apporte rien et peut être omise.

Une autre variante consiste à exposer une méthode egale(Montant $autre): bool plutôt que de comparer directement les propriétés, ce qui garde la possibilité d’ajouter plus tard une tolérance ou une devise sans casser les appelants.

Sur les projets où j’ai introduit cet objet valeur, la règle qui a le plus réduit les tickets de support n’était pas la classe elle-même mais l’interdiction stricte, en revue de code, de tout appel à round() en dehors de la conversion initiale.

En résumé

Un flottant pour représenter un prix fonctionne tant que personne ne regarde d’assez près les totaux. Un objet valeur en centimes entiers coûte une classe de plus dans le projet, mais il élimine une classe entière de bugs difficiles à reproduire, car ils dépendent de l’ordre exact des opérations. Ce sujet concerne le calcul du prix lui-même ; la gestion de la fiscalité multi-pays, avec ses taux de TVA variables selon le pays de livraison, reste un problème distinct qui mérite son propre traitement.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi