# Les antipatterns d’une extension qui mélange logique métier et affichage

> Calculs, requêtes et balisage HTML entremêlés dans les mêmes fonctions : les symptômes d'un couplage excessif, et comment le défaire sans tout réécrire d'un coup.

- Auteur : WordPress Développement
- Publié le : 2024-10-13
- Mis à jour le : 2026-09-30
- Catégorie : Extensions
- URL : https://www.wpmoderne.fr/extensions/antipatterns-extension-logique-metier-affichage-melanges/

## L’essentiel

- Un echo au milieu d'un calcul rend le code impossible à tester
- Chaque nouveau format de sortie oblige à dupliquer la logique
- Séparer ne veut pas dire multiplier les fichiers inutilement

Un développeur qui reprend une extension héritée reconnaît vite le symptôme : une fonction de trois cents lignes qui calcule un tarif dégressif, exécute une requête `WP_Query`, puis imprime directement du HTML avec des `echo` disséminés entre les conditions. Impossible d'en réutiliser le résultat ailleurs, impossible de le tester sans faire tourner tout un affichage, impossible d'en changer le format sans risquer de casser le calcul.

Ce mélange n'est pas une simple question de style. Il a un coût mesurable en maintenance : chaque évolution demandée — afficher le même tarif dans un e-mail, dans un export CSV, dans un widget — oblige à copier-coller la fonction plutôt qu'à la réutiliser. Voici les symptômes les plus fréquents de ce couplage, et la manière d'en sortir progressivement.

## Symptôme : le calcul renvoie du HTML plutôt qu'une valeur

> L'essentiel à retenir : Un echo au milieu d'un calcul rend le code impossible à tester ; Chaque nouveau format de sortie oblige à dupliquer la logique ; Séparer ne veut pas dire multiplier les fichiers inutilement

Voici le type de fonction que l'on rencontre dans une extension héritée. Le calcul d'une remise par paliers et la fabrication du balisage sont entremêlés, et rien ne sort de la fonction que du texte déjà imprimé :

```
function afficher_tarif_degressif( $produit_id, $quantite ) {
	$prix = (float) get_post_meta( $produit_id, '_prix_unitaire', true );

	echo '<div class="tarif">';
	if ( $quantite >= 100 ) {
		$prix *= 0.80;
		echo '<span class="remise">-20 %</span>';
	} elseif ( $quantite >= 10 ) {
		$prix *= 0.90;
		echo '<span class="remise">-10 %</span>';
	}
	echo '<strong>' . number_format( $prix * $quantite, 2, ',', ' ' ) . ' €</strong>';
	echo '</div>';
}
```

Tout y est : la règle métier (les seuils et les taux), l'accès aux données (`get_post_meta()`), le calcul sur des nombres à virgule et la mise en forme. Pour savoir si douze unités à cinq euros valent bien cinquante-quatre euros, il faut charger WordPress, créer un produit, capturer la sortie et la relire en texte. La règle de remise est aussi introuvable : elle se cache entre deux `echo`.

## Première étape : figer le comportement avant d'y toucher

Avant tout déplacement de code, on écrit un test qui décrit ce que la fonction produit aujourd'hui. Ce test, dit de caractérisation, ne juge pas si le résultat est beau ; il garantit que le refactoring ne le change pas par inadvertance. La fonction imprime : on capture donc la sortie avec la mise en tampon de PHP.

```
public function test_sortie_actuelle_conservee() {
	$id = self::factory()->post->create();
	update_post_meta( $id, '_prix_unitaire', '5' );

	ob_start();
	afficher_tarif_degressif( $id, 12 );
	$html = ob_get_clean();

	$this->assertStringContainsString( '-10 %', $html );
	$this->assertStringContainsString( '54,00', $html );
}
```

## Deuxième étape : extraire le calcul dans une fonction pure

Une fonction pure ne lit rien d'autre que ses paramètres et ne fait rien d'autre que retourner une valeur. On en profite pour remplacer les nombres à virgule par des centimes entiers, qui évitent les écarts d'arrondi sur l'argent :

```
final class Tarif_Degressif {

	// Seuil de quantité => pourcentage de remise, du plus grand seuil au plus petit.
	private const PALIERS = array( 100 => 20, 10 => 10 );

	public static function calculer( int $prix_unitaire_centimes, int $quantite ): array {
		$taux = 0;
		foreach ( self::PALIERS as $seuil => $remise ) {
			if ( $quantite >= $seuil ) {
				$taux = $remise;
				break;
			}
		}

		$total = (int) round( $prix_unitaire_centimes * $quantite * ( 100 - $taux ) / 100 );

		return array(
			'taux'           => $taux,
			'total_centimes' => $total,
		);
	}
}
```

Cette classe ne dépend pas de WordPress : elle se teste avec PHPUnit seul, sans base de données ni chargement de l'environnement, et en quelques millisecondes.

```
use PHPUnit\Framework\TestCase;

final class Tarif_Degressif_Test extends TestCase {

	public function test_aucune_remise_sous_dix_unites(): void {
		$this->assertSame(
			array( 'taux' => 0, 'total_centimes' => 4500 ),
			Tarif_Degressif::calculer( 500, 9 )
		);
	}

	public function test_dix_pour_cent_des_dix_unites(): void {
		$this->assertSame(
			array( 'taux' => 10, 'total_centimes' => 5400 ),
			Tarif_Degressif::calculer( 500, 12 )
		);
	}

	public function test_vingt_pour_cent_des_cent_unites(): void {
		$this->assertSame(
			array( 'taux' => 20, 'total_centimes' => 40000 ),
			Tarif_Degressif::calculer( 500, 100 )
		);
	}
}
```

## Troisième étape : isoler l'affichage

Le balisage devient une fonction qui reçoit le résultat du calcul et retourne une chaîne, sans rien imprimer. La fonction d'origine ne garde que le rôle de colle : lire la donnée WordPress, appeler le calcul, afficher le résultat.

```
function tarif_en_html( array $tarif ): string {
	$html = '<div class="tarif">';
	if ( $tarif['taux'] > 0 ) {
		$html .= '<span class="remise">-' . (int) $tarif['taux'] . ' %</span>';
	}
	$html .= '<strong>' . esc_html( number_format( $tarif['total_centimes'] / 100, 2, ',', ' ' ) ) . ' €</strong>';

	return $html . '</div>';
}

function tarif_en_texte( array $tarif ): string {
	return number_format( $tarif['total_centimes'] / 100, 2, ',', ' ' ) . ' €';
}

function afficher_tarif_degressif( $produit_id, $quantite ) {
	$prix = (int) round( (float) get_post_meta( $produit_id, '_prix_unitaire', true ) * 100 );

	echo tarif_en_html( Tarif_Degressif::calculer( $prix, (int) $quantite ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
```

Le test de caractérisation de la première étape doit passer sans modification : c'est la preuve que le comportement visible n'a pas changé. Les nouveaux formats coûtent désormais une ligne chacun : l'e-mail reprend `tarif_en_texte()`, l'export CSV lit directement les deux valeurs du tableau, le bloc ou le widget appelle `tarif_en_html()`. La règle de remise, elle, n'existe qu'à un seul endroit.

## Les autres symptômes du même couplage

- **Un `echo` dans un filtre.** Un filtre doit retourner une valeur ; un filtre qui imprime casse tout ce qui l'enchaîne après lui.
- **Un `exit` ou `die()` au milieu d'une fonction.** Le code appelant ne peut plus rien faire de l'erreur, et aucun test ne survit à la sortie brutale du processus.
- **La lecture de `$_POST` ou de `$_GET` dans le calcul.** La fonction dépend d'une requête HTTP ; passez les valeurs en paramètres, et lisez-les à la frontière de l'application.
- **Une requête `WP_Query` dans un fichier de gabarit.** Le gabarit décide alors de quelles données existent ; préférez une fonction qui prépare les données et un gabarit qui les affiche.
- **Des variables globales partagées** (`global $produit`) comme moyen de transmettre un état : chaque appel dépend de l'ordre des précédents.

## Séparer sans multiplier les fichiers

Le danger inverse guette celui qui découvre la séparation des responsabilités : fabriquer une interface, une usine, un dépôt et un service pour une règle qui tient en vingt lignes. Dans l'exemple ci-dessus, deux éléments suffisent : une classe de calcul, sans dépendance, et une ou deux fonctions d'affichage. Le critère pratique est de pouvoir répondre à deux questions : peut-on tester la règle sans charger WordPress, et peut-on changer le balisage sans relire le calcul ? Si oui, la séparation est faite ; ajouter des couches ne fait que disperser le code.

Autre règle de prudence : on ne réécrit pas tout d'un coup. On commence par la fonction qui change le plus souvent ou qui cause le plus de bogues, avec son test de caractérisation, et on avance fonction par fonction. Le code qui fonctionne et que personne ne modifie peut attendre.

> Une fonction qui calcule et qui affiche en même temps finit toujours par être copiée : pas par paresse, mais parce qu'on ne peut pas réutiliser la moitié seulement.

## Conclusion

Mélanger calcul et affichage ne coûte rien le premier jour et coûte cher ensuite : des tests impossibles, une logique dupliquée à chaque nouveau format, des règles métier introuvables. Le chemin de sortie est court : figer le comportement par un test, extraire le calcul en fonction pure, isoler le balisage, garder une fine couche de colle. Gardez la mesure : deux fichiers bien séparés valent mieux que huit fichiers vides de sens.
