Le dépoussiérage de mon appli de bioinformatique : partie 3 – Refonte et packaging de BioTools

Il y a quelques semaines, je me suis attaquée à la migration d’un plug-in de BioPHP, BioTools, mon portage PHP 8 / Symfony des minitools BioPHP de Joseba Bikandi. Le projet était bloqué en Symfony 4.4, et sa dépendance sœur amelaye/biophp avait elle-même été migrée progressivement jusqu’à SF7 dans le cadre de sa propre montée en version. Résultat : faire tourner les deux ensemble était devenu impossible — Composer ne résout qu’une version de symfony/framework-bundle pour tout l’arbre de dépendances, et les deux projets ne s’accordaient plus sur laquelle.

L’objectif de cette migration n’était donc pas seulement de rattraper le retard de version : c’était aussi l’occasion d’achever le travail commencé sur biophp et de vérifier qu’il tenait en conditions réelles. Ce que sa propre suite de tests ne pouvait pas garantir — et la suite l’a prouvé.

Spoiler : j’aurais pu sauter directement en SF7. J’ai quand même fait la montée progressive — SF4 → SF5 → SF6 → SF7 —, parce que j’aime bien souffrir proprement et avoir un compte rendu pour chaque étape.

La montée progressive, ou l’art de créer du travail

La contrainte de départ était simple : biophp exigeait symfony/*: ^7.4 et php: ^8.2. Impossible de viser SF5 avec ça. Solution : créer des branches de compatibilité sur biophp (symfony5-compat, symfony6-compat) qui élargissent uniquement les contraintes de version sans toucher au code — ou presque.

SF5 s’est passé relativement bien. Les vrais problèmes commençaient :

  • sensio/framework-extra-bundle était déjà mort dans le projet (l’annotation router était désactivée, et les @Route étaient chargées nativement) — retiré sans cérémonie.
  • L’arsenal Behat entier (behat/behat, behat/mink, et ses cinq amis) : aucun fichier .feature, aucun behat.yml, aucune classe Behat\ référencée nulle part. Toolchain mort. Retiré.
  • geshi/geshi : une alerte de sécurité (CVE-2025-2123, XSS dans contrib/cssgen.php) sans version corrigée disponible. Le script vulnérable n’est pas atteignable depuis le projet. Ignorée explicitement et documentée dans config.audit.ignore — pas question de désactiver l’audit en entier pour un seul paquet.

SF6 a amené sa première modification de code réel dans biophp : AmelayeBioPHPBundle::getContainerExtension() n’avait pas de type de retour déclaré, alors que la classe parente impose ?ExtensionInterface depuis SF6. PHP étant PHP, ça casserait au chargement du kernel — mais comme les tests de biophp ne bootent jamais de vrai kernel, ce bug était resté invisible pendant toute la migration SF7 de biophp. Je l’ai retrouvé deux fois (une fois sur la branche de compat, une fois sur develop en arrivant en SF7), ce qui m’a légèrement amusée.

SF6 a aussi tué AbstractController::get() — plus déprécié, retiré. Dix-huit appels à $this->get('form.factory')->create(XxxType::class) dans cinq contrôleurs, remplacés mécaniquement par $this->createForm(XxxType::class), qui fait exactement ça depuis Symfony 2.

SF7 : conversion des 19 annotations @Route en attributs #[Route(...)] (inévitable, le chargeur type: annotation ayant été retiré), correction de deux signatures de Constraint, migration de la suite PHPUnit de la version 7 à la 11. Cette dernière a révélé que quelques tests passaient en vert pour de mauvaises raisons — PHPUnit 9 convertissait automatiquement les warnings PHP en exceptions catchables, ce que PHPUnit 10+ ne fait plus. Les tests « fonctionnaient » grâce à un comportement de PHPUnit qui n’avait rien à voir avec la logique testée.

Le vrai sujet : ce que j’ai trouvé dans le code original

Une fois la migration terminée, j’ai voulu transformer biotools en composant Packagist propre — ce qui impliquait de comparer chaque manager à sa source originale sur biophp.org. Et là, les choses sont devenues intéressantes.

Défaut n°1 : la conversion Celsius → Fahrenheit est fausse.

F° = 32 + (C° × 0.555)

0.555 est approximativement 5/9, le facteur pour aller dans l’autre sens (Fahrenheit → Celsius). Appliqué dans ce sens, 100 °C donne 87,5 °F au lieu de 212 °F. La conversion inverse farhenToCenti() dans le même fichier est, elle, correcte — même facteur, bon sens. La source officielle publie ce résultat depuis au moins 2009.

Défaut n°2 : la conversion millibars → pouces de mercure surestime de 33%.

Le facteur utilisé est 0.0394, qui est en réalité le facteur millimètre → pouce. Il manque une étape de composition : 1 mbar = 0.750062 mmHg × 0.03937 in/mm = 0.02953 inHg. Le facteur publié gonfle tous les résultats d’un tiers.

Défaut n°3 : division par zéro non protégée.

Dans distance_among_sequences/index.php, la fonction standar_frecuencies() calcule une somme de fréquences, puis divise par cette somme. Si le tableau est vide ou ne contient que des zéros, $sum vaut 0. Sous PHP 5 et 7 : un warning et INF, silencieux. Depuis PHP 8 : DivisionByZeroError fatale.

Défaut n°4 : zéro validation d’entrée dans l’ensemble du corpus.

J’ai cherché is_string(, is_array( et throw new dans les 20 sources originales. Aucun résultat. Chaque fonction appelle directement strlen(), substr(), strpos(), preg_replace() sur ce qu’on lui donne, quelle qu’en soit la nature. Sous PHP 7 : des warnings tolérés. Sous PHP 8 : TypeError fatale sur presque toutes les entrées invalides.

C’est exactement pour ça que mes tests PHP 8 échouaient depuis la migration SF5 : les tests passaient un tableau à une fonction attendant une chaîne, espéraient attraper une \Exception métier, et PHP levait un TypeError (qui étend \Error, pas \Exception) avant que le code n’ait rien pu vérifier. Les tests étaient verts sous PHPUnit 9 parce que ce dernier convertissait les warnings en exceptions. Ils ne testaient rien, et PHPUnit 10 l’a révélé.

Défaut n°5 : imagedestroy() déprécié depuis PHP 8.5.

La fonction n’a plus aucun effet depuis PHP 8.0 et émet une notice de dépréciation depuis 8.5. Tous les outils graphiques se terminent avec cet appel. Il suffit de le supprimer — ce que j’ai fait en passant les graphiques de GD à SVG (cf. plus bas).

Et une coquille : dans la table de référence protéine/ADN, la cellule « 100 kDa » donne « 2.7 bp » au lieu de « 2.7 kb ». Les deux autres lignes sont cohérentes avec la règle énoncée.

J’ai consigné tout ça dans un document séparé en anglais, à destination des auteurs d’origine — même si le projet n’a plus beaucoup bougé depuis 2009 (ce que je lui ai reproché 10 ans après en l’occurrence).

Grâce à Claude, mes compétences limitées dans le domaine ont pu être soulevées !

Ce que ça a changé concrètement

Pour le portage, j’ai corrigé les deux formules et ajouté de la validation d’entrée sur les 19 méthodes publiques concernées. Chaque divergence avec le legacy est documentée et commentée dans le code. Les sources originales sont conservées verbatim dans legacy/ pour la continuité de licence (GNU GPL v2).

Les graphiques sont passés de GD à SVG : suppression de la dépendance ext-gd, noms de fichiers uniques (les noms fixes permettaient aux requêtes concurrentes de s’écraser mutuellement), et gestion correcte du chemin de sortie — SkewsManager écrivait en dur dans created_files/ relatif au répertoire courant, donc dans le vide la plupart du temps, sans lever la moindre erreur. En bonus, la déduplication des points SVG donne un gain de taille de 19× sur le graphique CGR.

Au bout du compte : 129 tests, 0 erreur, 0 échec, 0 dépréciation. Le composant s’appelle désormais amelaye/biotools, il sera publié sur Packagist une fois la dépendance sur biophp figée sur un tag.