On hérite d’un POC. Un prototype qui « marche » : il déroule le parcours, il produit le bon résultat à l’écran, le client est content de la démo. La consigne initiale : « réutilisez ce qui existe, il ne reste qu’à finir ».
Audit du POC, première heure :
- 0 % de couverture de tests ;
- des contrôleurs de 475 à 545 lignes, faisant tout — validation, appels API, mapping, rendu ;
- des tokens en dur dans le code, des secrets dans le
.envcommité ; - des données sensibles stockées en clair en session ;
- plusieurs appels API séquentiels prenant 3 à 15 secondes bout à bout.
« Réutiliser » ça ? On a renégocié la consigne : partir des bonnes idées, refondre l’implémentation. La nuance n’est pas cosmétique — elle change tout le projet.
Un POC répond à « est-ce possible ? », pas à « est-ce maintenable ? »
Un POC est un dispositif de réponse à une question : le parcours est-il faisable dans les délais ? l’API du partenaire renvoie-t-elle ce qu’il faut ? le design tient-il ? Une fois la réponse obtenue (« oui »), le POC a fini son travail. Son code n’a jamais eu pour but de durer ; il a eu pour but de prouver.
Le piège, c’est de confondre le résultat du POC (l’intention validée : voilà le parcours, voilà les écrans, voilà les appels) avec son implémentation (un contrôleur de 545 lignes avec un token en dur). L’intention est précieuse, on la garde. L’implémentation est jetable, on la refond.
Concrètement, la refonte a suivi une trajectoire en deux temps : d’abord mettre sous tests le comportement existant (filet de sécurité), ensuite découper par extractions successives sans changer le comportement observable.
Découper un contrôleur obèse, une extraction à la fois
Un contrôleur de 600+ lignes ne se refond pas d’un bloc — on le vide par petites MR, chacune extrayant une responsabilité vers un objet testable hors du framework. Exemple : la normalisation de téléphone, un helper privé noyé dans le contrôleur, devient un Value Object immutable.
// AVANT — méthode privée dans un contrôleur de 662 lignes, non testable seule
private function normalizePhone(string $raw): ?string
{
// 15 lignes de strip + regex + préfixage…
}
// APRÈS — Value Object isolé, testé par 25 cas, contrôleur allégé de 19 lignes
$phone = PhoneE164::tryFromUserInput($input)?->value; // null si invalide → flash
Même mécanique pour la matrice de pièces justificatives obligatoires : 25 lignes de règles métier (« quelles sections sont requises selon le profil de l’adhérent ») extraites du contrôleur vers un service pur, dont la logique de calcul devient enfin testable en isolation :
final class UploadValidator
{
/** @return list<string> les sections obligatoires selon le contexte */
public function computeRequiredSections(
bool $contratActuel,
bool $hasConjoint,
array $enfants,
): array {
$sections = [
self::SECTION_IDENTITE,
self::SECTION_ATTESTATION,
self::SECTION_NAISSANCE,
self::SECTION_RIB,
];
if ($contratActuel) {
$sections[] = self::SECTION_RADIATION;
}
if ($hasConjoint) {
$sections[] = self::SECTION_CONJOINT_SITUATION;
$sections[] = self::SECTION_CONJOINT_ATTESTATION;
}
foreach ($enfants as $i => $enfant) {
$sections[] = "enfant_{$i}_attestation";
if ($enfant->scolariteRequise()) { // 18-28 ans
$sections[] = "enfant_{$i}_scolarite";
}
}
return $sections;
}
}
Chaque MR de ce type : −20 à −30 lignes dans le contrôleur, +10 à +25 tests ciblés, 0 régression. Cumulées, une dizaine de ces extractions ont fait fondre le contrôleur principal de 686 à 612 lignes tout en transformant des helpers privés inaccessibles en services couverts. Et en chemin, on a posé des couches DDD light (Domain / Application / Infrastructure), des interfaces sur les clients tiers (DIP), des enums PHP 8.1 à la place des public const à gogo.
Le garde-fou qui rend tout ça possible : la CI dès la première MR
Rien de ce qui précède n’est sûr sans filet. La toute première chose mise en place, avant même la première extraction, c’est un pipeline CI vert obligatoire sur chaque MR : tests + analyse statique + style. Ça paraît prématuré sur un projet qui n’a aucun test — c’est exactement l’inverse. Elle impose la discipline pour toutes les MR suivantes : impossible de merger une extraction qui casse un comportement, parce que le test de régression écrit avant l’extraction l’attrape.
Quelques mois plus tard, le bilan :
- tunnel applicatif en recette client, déroulé de bout en bout ;
- plusieurs centaines de tests unitaires et d’intégration (le contrôleur de départ en avait zéro) ;
- un harnais de 57 tests d’intégration sur l’API PDF externe, qui détecte les régressions du prestataire en minutes ;
- pipeline CI vert sur chaque MR ;
- secrets sortis du code vers la config d’environnement, données sensibles chiffrées, contrôleurs découpés en services à responsabilité unique.
Lessons learned
- Un POC sans tests n’est pas un point de départ, c’est une direction. Il prouve qu’un chemin est viable ; il ne fournit pas une base de code sur laquelle bâtir. Préservez l’intention (le parcours, les écrans, les intégrations validées), refondez l’implémentation.
- Renégociez « réutiliser » en « repartir des bonnes idées ». Le coût caché de « finir » un POC dépasse presque toujours celui de le refondre proprement — mais il se paie plus tard, en bugs et en peur de toucher au code. Posez le débat tôt, chiffres à l’appui.
- Refondez par extractions successives, pas d’un bloc. Une responsabilité à la fois, sortie du contrôleur vers un objet testable, sans changer le comportement observable. Chaque MR est petite, revue, vert. Le contrôleur maigrit ligne après ligne.
- Installez la CI dès la première MR, même (surtout) quand il n’y a aucun test. Elle impose la discipline pour les suivantes et transforme « j’espère que je n’ai rien cassé » en « le pipeline le confirme ».
- Les tests ne sont pas un coût, ils permettent de livrer plus vite — sans peur. Le filet de régression est ce qui autorise de découper un contrôleur de 600 lignes sans trembler.
- L’audit initial d’un POC est une mine. 80 % des problèmes (sécurité, perf, dette) y sont déjà visibles en une journée de lecture. Faites-le, écrivez-le, et servez-vous-en comme feuille de route priorisée.
#tdd #refactoring #ddd #legacy #symfony #architecture