Comment une simple erreur 500 sur une API Symfony m’a entraînée dans une modernisation complète — Composer, Deployer, migration PHP 8 et pièges Doctrine compris.
Cette fois, je savais dès le départ à quoi m’attendre. Un projet qu’on laisse dormir plusieurs années ne se réveille jamais avec un simple git pull : il se réveille en mission archéologique, couche après couche. Alors avant même de lancer la première commande, j’avais le bon état d’esprit : creuser, documenter, ne pas m’énerver quand une correction en révèle trois autres en dessous. Et cette fois, j’avais un luxe que je n’avais pas les fois précédentes où j’ai fouillé seule dans un vieux projet : une IA à mes côtés pour diagnostiquer plus vite, remonter aux vraies causes sans tourner en rond, et garder une trace claire de chaque strate retirée.
Au départ de cette fouille : une erreur 500 anodine sur api.amelayes-biophp.net, mon API bioinformatique en Symfony 4 / API Platform. À l’arrivée : une remise à plat de toute ma chaîne, de Composer jusqu’au fond de mon deploy.php, une bibliothèque republiée et une migration Symfony sur trois versions d’un coup. Je vous raconte tout, parce que le vrai fil rouge de cette histoire n’est pas un bug précis : c’est la dette technique et la dépréciation qui s’accumulent en silence sur un projet qu’on laisse vivre sa vie.
Ha oui et pour les dictateurs de la bonne conscience (car j’y ai eu droit un jour à la question) : Mai pourkoua vou ne l’avai pa fini ???? Parce que c’est un side-project donc qui dit side-project dit sur son temps libre, parce que ma maman a eu de graves soucis de santé, parce que mon papa a quitté ce monde, parce que la maison, parce que les clients c’est plus important. Et à moment donné j’ai eu envie de m’évader sur des blocs virtuels empilés les uns sur les autres. Et ça c’est aussi un beau side-project. Sans rire. Ce ne sont pas des excuses, mais juste la vie. Et je vais être honnête : sans l’IA, je n’aurais pas remis les mains dans ce cambouis.
Le déclencheur : Composer 1 n’existe plus
Tout part d’une envie simple : mettre à jour l’API. Je lance un composer install de routine, et là, mur. Une vingtaine de paquets « introuvables ». Symfony, Doctrine, API Platform… tout d’un coup « could not be found ».
La vraie cause, tout en haut de la sortie :
Support for Composer 1 has been shutdown on September 1st 2025.
Mon Composer était resté en version 1, et Packagist a coupé le support. Mon vendor/ était bancal parce qu’un vieux composer install avait échoué à moitié, et c’est ça qui faisait planter l’API. Première leçon, valable pour toute la suite : la dépréciation ne prévient pas, elle attend que vous reveniez sur le projet pour se rappeler à vous.
Le piège s’est doublé aussitôt : sur mon Mac, macOS n’était plus supporté par Homebrew, donc brew cassé, donc composer self-update impossible. Pour réparer un outil, j’avais besoin d’un autre outil que je ne pouvais plus mettre à jour. Solution pragmatique : récupérer le .phar de Composer 2 en direct, sans passer par Homebrew.
Le deploy.php : un fichier plein de décisions devenues obsolètes
C’est ici que l’histoire devient vraiment parlante. Mon fichier de déploiement Deployer n’était pas juste cassé : il était rempli de déclarations qui avaient eu une bonne raison d’exister un jour, et qui étaient devenues du poids mort, voire nuisibles, sans que rien ne me le signale.
Le hostname manquant. host('bioapi-prod') n’avait pas de ->set('hostname', ...). Ça marchait avant, grâce à un alias SSH dans un ~/.ssh/config sur une autre machine — jamais versionné, donc invisible dès que j’ai changé de poste. Rien dans le fichier ne documentait cette dépendance externe.
Le ->stage('prod') devenu un vestige. Syntaxe standard en Deployer 6 pour sélectionner un environnement. En Deployer 7, ce mécanisme de stage disparaît : dep deploy prod devient dep deploy bioapi-prod, via l’alias d’hôte directement. Le ->stage() traînait sans plus rien piloter.
restart_fpm et php7.4-fpm en dur. Une tâche custom rechargeait php7.4-fpm après chaque déploiement. Logique tant que le serveur tournait en PHP 7.4. Après la migration vers PHP 8.1, cette tâche était non seulement inutile, elle était fausse — elle redémarrait un service PHP qui n’était plus celui servant l’application. Il a fallu retirer la définition de la tâche et son hook after(), les deux ensemble, sous peine de faire planter le déploiement sur une tâche fantôme.
Le chemin de recipe périmé. Le require 'recipe/symfony4.php' a dû devenir le recipe générique recipe/symfony.php de Deployer 7. Détail apparemment mineur, mais premier point de rupture classique d’une montée de version majeure.
Le ssh_multiplexing en aller-retour. Mis à false en Deployer 6 pour contourner un souci Windows, il a dû repasser à true pour Deployer 7 et son nouveau système de verrouillage. Une déclaration héritée d’un vieux bug, à inverser pour un bug nouveau.
deploy:lock / deploy:unlock, retirés carrément. Deployer 7 a un bug documenté où deploy:lock se redéclenche seul après deploy:writable, provoquant une GracefulShutdownException sur un déploiement pourtant sain. Comme je suis seule à déployer, j’ai retiré ces tâches de la liste explicite, en gardant le filet after('deploy:failed', 'deploy:unlock').
Ce qui me frappe : chacune de ces lignes avait été juste, un jour, pour une bonne raison. Aucune n’était une erreur de débutante. C’est exactement ça, la dette technique : pas du mauvais code, mais des décisions correctes qu’on n’a pas révisées quand leur contexte a changé.
Les migrations Doctrine : deux pièges de dépréciation coup sur coup
Le déploiement passait enfin la phase code, et calait sur la base de données. Deux problèmes distincts, tous deux liés à l’évolution de Doctrine Migrations.
Premier piège : la config du bundle. La syntaxe de doctrine-migrations-bundle avait changé. Les anciennes clés dir_name et namespace ont été remplacées par migrations_paths. Tant que la config n’était pas mise à jour, le bundle ne trouvait plus ses migrations correctement.
La vraie cause du 500, cachée derrière un fichier vide
Une fois Composer, Deployer et les migrations stabilisés, l’API se déployait — et plantait toujours en 500. Cause réelle : une variable CORS_ALLOW_ORIGIN manquante, bloquée par un .env.local.php vide sur le serveur, prioritaire sur le .env normal (comportement standard de Symfony : le fichier compilé prime, même vide). Un fichier de cache oublié par un ancien déploiement suffisait à masquer toute la config. Il a fallu aussi relancer OPcache après le réchauffement du cache, sinon le serveur continuait de servir l’ancienne version compilée.
Côté demo : biophp lui-même n’était pas épargné
Le site demo.amelayes-biophp.net avait sa propre saga. Des dump() dans les templates Twig faisaient planter la prod (remplacés par une macro tree maison). Plus profond : des objets biophp refusaient de se sérialiser, à cause de types non-nullables sur des getters pouvant renvoyer null selon le schéma Doctrine réel. Les types ont été alignés avec la base (v0.1.15 → v0.1.16), getSeqSet() a cessé de copier en tableau pour renvoyer son ArrayIterator d’origine, et plusieurs DataFixtures ont récupéré un : void de retour manquant. Le bundle EasyAdmin, jamais utilisé, a été retiré — encore une déclaration qui traînait pour rien.
La remise à plat de fond : Symfony 4.4 → 6.4, PHP 7.4 → 8.1
Restait le vrai sujet : le projet tournait encore en Symfony 4.4 et PHP 7.4, deux versions en fin de vie. Migration par paliers : 4.4 → 5.4 → 6.4 côté Symfony, 7.4 → 8.1 côté PHP, avec à chaque étape un audit à sec (composer update --dry-run) avant d’écrire quoi que ce soit — repérer les paquets qui changent, ceux qui refusent de se résoudre, et dans le code les fonctions dépréciées ou supprimées (each(), create_function(), propriétés dynamiques non déclarées). Un platform.php a été ajouté à composer.json pour figer la version PHP ciblée et éviter toute dérive. Au passage, découverte que le site demo tournait en réalité sur PHP 7.3 — pas 7.4 — à cause d’un SetHandler de vhost Apache resté sur l’ancienne version.
La fin : Migration vers PHP 8.5 et mise à jour de tout le composer vers la nouvelle version d’Api Platform
Deuxième piège : une migration qui plante sans rien casser. Voici le message qui m’a fait perdre du temps :
[bioapi-prod] [notice] Migrating up to DoctrineMigrations\Version20260807140000 [bioapi-prod] [warning] Migration … was executed but did not result in any SQL statements. [bioapi-prod] exit code 255 (Unknown error) ERROR: Task database:migrate failed!Le comportement de doctrine/migrations a évolué : une migration dont le up() ne produit aucun SQL est désormais traitée strictement. Là où une ancienne version passait en silence, la nouvelle émet un warning et renvoie un code d’erreur 255 — ce qui fait échouer la tâche database:migrate de Deployer alors que, sur le fond, rien n’est cassé. Un classique de dette technique : ce n’est pas mon code qui a changé, c’est la sévérité de l’outil autour.
Le vrai fond du problème : array déprécié au profit de json. Sous ce warning se cachait une vraie migration de données. Le type de colonne Doctrine array — qui stocke via serialize() de PHP — a été déprécié puis abandonné au profit du type json. Les anciennes données sérialisées en base ne sont plus lisibles telles quelles par le nouveau type. J’ai donc dû modifier deux entités pour passer leurs propriétés concernées de array à json, et prévoir la conversion des données existantes. Encore une décision parfaitement valable à l’époque (le type array était l’usage courant) devenue une dette le jour où Doctrine a tourné la page.
Une fois que tout est ok, on y est ! Yalla !!! Pour le moment …
Ce que cette histoire dit de la dette technique
S’il faut retenir une chose : sur un projet perso laissé plusieurs mois, ce n’est presque jamais le code métier qui pourrit en premier. C’est l’écosystème autour — gestionnaire de paquets, outil de déploiement, version de PHP, un bundle oublié, une variable dans un fichier de cache, un type de colonne Doctrine — qui se désynchronise petit à petit, une décision correcte à la fois, jusqu’à ce que la dépréciation d’un seul maillon fasse tomber tous les autres.
Le deploy.php en est le symbole : chaque ligne avait une raison d’être, à un instant T, et il a fallu les relire une à une, des années plus tard, pour savoir lesquelles étaient encore vraies. C’est sans doute le vrai travail de fond de la maintenance : pas ajouter des fonctionnalités, mais se demander régulièrement si les décisions figées dans la config tiennent toujours.
La suite de ce chantier est déjà sur ma liste : cette fois-ci terminer pour de bon la librairie et enrichir le site de démo, avec les tests. Mais ça, c’est une autre histoire.
















