Composer en production: les coulisses d'un conflit résolu
Pas un bug applicatif exotique, pas une faille zero-day — un conflit de versions qui a paralysé un déploiement, fait transpirer une équipe, et parfois coûté plusieurs heures de week-end. Je ne vous raconte pas ça pour alimenter une fascination pour le chaos, mais pour poser une évidence qui circule dans les équipes d'ingénierie bien menées: la capacité à diagnostiquer et résoudre ces conflits est, sur le marché actuel, un marqueur fiable de séniorité. Et c'est une compétence qui s'apprend, pas qui s'improvise.
Alors avant d'entrer dans la technique, je vous dois une promesse. Je ne vais pas vous abreuver de commandes magiques. Ce qui suit est la méthode que je vois mettre en œuvre par les candidats qui décrochent les postes — et, plus important encore, par ceux qui les gardent.
Anatomie d'un blocage: pourquoi composer install échoue en production
Un conflit de dépendances en production commence rarement par une ligne de code exotique. Il commence par une incompréhension tranquille de ce que fait — et surtout de ce que ne fait pas — votre fichier composer.lock.
Quand composer.lock est présent dans votre projet, la commande composer install lit ce verrou et installe les versions exactes qui y sont consignées. C'est un contrat entre votre poste de développement, votre pipeline d'intégration continue et votre serveur de production: tant que le verrou n'est pas modifié, tout le monde travaille avec les mêmes paquets, aux mêmes versions, avec les mêmes hashs de contenu. C'est précisément cette propriété qui rend l'opération reproductible.
Le problème surgit quand un développeur, parfois le plus expérimenté, lance composer update au lieu de composer install. Ces deux commandes n'ont rien d'équivalent. composer update interroge les dépôts pour récupérer les versions les plus récentes compatibles avec les contraintes de votre composer.json, puis réécrit composer.lock. Résultat: le verrou a changé, et avec lui la version qui sera installée en production à la prochaine mise en service. Si la mise à jour a introduit une régression subtile — un changement de signature, une dépendance devenue incompatible — personne ne l'a vue passer, parce qu'elle n'a été testée nulle part.
En production, composer install n'est pas une option par défaut — c'est une discipline d'équipe.L'autre scénario qui revient avec une régularité confondante, c'est celui du conflit Git sur composer.lock. Deux branches modifient le verrou, l'une fusionne, l'autre doit résoudre manuellement, et quelqu'un — souvent pressé — choisit de garder « la version la plus récente » sans vérifier la cohérence avec composer.json. Le verrou est désormais désynchronisé de la déclaration de dépendances. Avant Composer 2.5, cette situation passait parfois inaperçue: composer install faisait de son mieux et installait les paquets présents dans le verrou, même si certains n'étaient plus requis par composer.json. Depuis la version 2.5, la commande échoue proprement lorsqu'un paquet requis par composer.json est absent du verrou. C'est un filet de sécurité, pas une punition. Pour beaucoup d'équipes, ce changement a brutalement transformé des fusions « qui marchaient avant » en échecs de pipeline — d'où l'impression d'une régression là où il n'y en a pas.
Diagnostic précis: prohibits et why pour identifier le coupable
Quand un conflit de dépendances se déclare en production, la première impulsion est souvent mauvaise: relancer un composer update avec une option trouvée sur Stack Overflow, croiser les doigts, et pousser en espérant que ça passe. C'est exactement la posture qui distingue le candidat junior du candidat senior en entretien.
Avant toute chose, vous devez identifier le coupable. Deux commandes sont vos meilleures alliées.
composer prohibits, alias composer why-not, répond à la question inverse de composer requires: « Pourquoi ce paquet, à cette version, ne peut-il pas être installé? » La commande liste les contraintes qui bloquent l'installation demandée. La documentation officielle donne un exemple parlant: composer prohibits php 8 permet de déterminer quels paquets empêchent une montée vers PHP 8. C'est un outil de diagnostic, pas une baguette magique — il vous dit qui bloque, pas comment débloquer.
composer depends, alias composer why, fait l'inverse: il vous montre quels paquets dépendent d'un paquet donné. Avec l'option --tree (ou -t), vous obtenez l'arbre complet des dépendances, ce qui devient vite indispensable quand un paquet intermédiaire est responsable de la tension. J'ai vu des candidats perdre un temps précieux à essayer de mettre à jour un paquet racine qui n'était en réalité qu'une victime collatérale d'une dépendance transitive — la dépendance racine, elle, n'avait aucun bloqueur direct.
Diagnostic avant patch. Toujours.
Le candidat qui, en entretien, commence son récit d'incident par « j'ai lancé composer prohibits pour identifier le bloqueur, puis composer depends --tree pour comprendre la chaîne » montre quelque chose de rare: une méthode reproductible. C'est cette méthode qui transforme un récit de survie en démonstration de compétence.
Le piège des contraintes: quand composer.json vous joue des tours
Si vous voulez comprendre pourquoi un conflit se déclenche aujourd'hui plutôt qu'hier, regardez votre composer.json. Les contraintes que vous y avez écrites racontent une histoire — et cette histoire peut vous jouer des tours pendant des mois avant de mordre.
Une contrainte comme *, >=3.4 ou dev-master n'a pas de borne supérieure. Elle autorise donc des versions majeures futures, y compris celles qui introduiront des ruptures de compatibilité. La documentation recommande explicitement d'écrire ^3.4 plutôt que >=3.4: la première accepte les versions jusqu'à 3.999, mais exclut 4.0 et toute version supérieure. C'est une discipline d'écriture qui paraît anodine en relisant le code, mais qui devient cruciale quand un composer update traverse une frontière majeure par accident.
Petit rappel utile à garder en tête pendant vos revues de code:
^1.2.3équivaut à>=1.2.3 <2.0.0^0.3équivaut à>=0.3.0 <0.4.0(la borne supérieure change en dessous de 1.0)~1.2équivaut à>=1.2 <2.0.0~1.2.3équivaut à>=1.2.3 <1.3.0
Ces syntaxes ne sont pas interchangeables. Le candidat qui les distingue en entretien, sans devoir vérifier la documentation, est un candidat qui a déjà causé — ou évité — un incident. À l'inverse, celui qui confond ~ et ^ me dit, en creux, qu'il n'a jamais eu à expliquer la différence à une équipe qui venait de subir une mise à jour cassante.
Vérification de l'environnement: le rôle critique des paquets de plate-forme
Quand un conflit se produit en production mais pas en local — ou inversement — le suspect numéro un n'est pas votre code applicatif: c'est votre environnement. Composer traite PHP, les extensions ext-*, les bibliothèques système lib-*, ainsi que composer, composer-plugin-api et composer-runtime-api comme des paquets de plate-forme virtuels, dont les versions sont déterminées par l'environnement où Composer s'exécute.
Concrètement: si votre CI tourne sous PHP 8.2 et que votre production tourne sous PHP 8.1, un paquet qui exige PHP 8.2 s'installera sans broncher dans la CI et échouera en production. Composer ne dispose pas, par défaut, d'un moyen de réconcilier ces deux visions du monde — c'est à vous d'aligner vos environnements.
La commande composer check-platform-reqs est votre alliée dans cette situation. Contrairement à composer install et composer update, elle ignore config.platform et contrôle la plate-forme réellement utilisée. C'est précisément ce qui la rend pertinente après l'installation sur un serveur de production: vous voyez enfin la vérité du terrain, pas celle simulée par votre configuration locale.
Une précision qui mérite d'être soulignée: config.platform-check vaut php-only par défaut. Avec la valeur true, vous contrôlez aussi la présence des extensions. Pour un projet qui s'appuie sur des extensions critiques — pensez à ext-intl, ext-redis, ext-amqp — passer à true est souvent un petit investissement qui évite de gros ennuis. Je ne compte plus les entretiens où le candidat m'a parlé d'un incident qui s'est résolu, après des heures de recherche, par la simple installation d'une extension oubliée sur le serveur de production.
Un dernier point, et il est non négociable: ne recommandez jamais --ignore-platform-reqs comme correctif de production. Cette option force l'installation malgré des exigences PHP, ext-* ou lib-* non satisfaites. Vous obteniez peut-être un vendor/ qui se déploie, mais vous importez le problème dans votre code applicatif — et il explosera ailleurs, plus tard, plus loin.
Stratégies de mise à jour: maîtriser les options sans empirer la situation
Une fois le diagnostic posé et l'environnement aligné, vous allez vouloir mettre à jour. Et là, le diable se cache dans les options. Trois d'entre elles méritent qu'on s'y arrête sérieusement.
--with-dependencies (alias -w) met à jour les dépendances des paquets explicitement demandés, sauf les dépendances racines. C'est l'option raisonnable quand vous savez ce que vous visez et que vous voulez maîtriser le périmètre du changement.
--with-all-dependencies (alias -W) va plus loin: elle inclut également les dépendances racines concernées. C'est tentant, parce qu'elle élargit le champ des paquets susceptibles d'être mis à jour — donc de débloquer une situation. Mais attention: cette option n'est pas un correctif universel. Elle élargit le périmètre, elle ne le résout pas magiquement. Un candidat qui dit « il suffit de lancer -W » en entretien montre qu'il a lu une réponse Stack Overflow, pas qu'il a compris le mécanisme.
--minimal-changes (alias -m) limite la mise à jour aux changements strictement nécessaires: les dépendances déjà verrouillées sont conservées si elles restent compatibles, sauf les paquets explicitement ciblés lors d'une mise à jour partielle. Dans un contexte de production, c'est souvent l'option la plus sage — parce qu'elle minimise la surface de risque. Et c'est précisément l'argument à mettre en avant en entretien: non pas « j'ai utilisé -m », mais « j'ai choisi -m pour limiter la portée de la mise à jour et garder la possibilité de revenir en arrière rapidement ».
Une fois votre conflit résolu et votre verrou stabilisé, il reste un geste qui sépare le déploiement correct du déploiement excellent: la régénération de l'autoloader en mode optimisé. L'option --optimize-autoloader (alias -o) génère une classmap à partir des fichiers PSR-0/PSR-4 de votre projet. La documentation officielle la recommande particulièrement pour la production, parce qu'elle réduit le travail de résolution de chemins que l'autoloader doit effectuer à chaque requête.
Deux précautions toutefois, et elles sont de bon sens. Une classmap autoritaire (-a, pour --classmap-authoritative) ne doit pas être combinée avec le cache APCu de l'autoloader: la cohabitation peut provoquer des erreurs de classe introuvable dans certaines configurations, lorsque l'autoloader cherche une classe dans une classmap déclarée exhaustive alors qu'elle a été régénérée ailleurs. C'est un piège silencieux qui se manifeste sporadiquement, souvent au pire moment. Et ne promettez jamais de gain de performance chiffré sans avoir mesuré sur votre propre application. Les chiffres que vous lirez dans des benchmarks externes ne valent rien face à votre charge, votre code, votre configuration matérielle. Les candidats crédibles disent « on a mesuré X millisecondes sur le cold start » — pas « ça divise le temps de chargement par trois ».
Le plan d'action pour ne plus revivre la même crise
Quand je prépare un développeur PHP à un entretien où l'on va lui demander de raconter un incident de production, voici la trame que je lui propose. Elle fonctionne aussi bien pour résoudre un conflit que pour en parler avec lucidité.
1. Reproduire l'incident en isolation. Avant toute chose, vérifiez que vous pouvez reproduire le conflit hors production — idéalement dans un environnement jetable, sans config.platform qui maquillerait la réalité. composer check-platform-reqs est votre point de départ, pas votre conclusion.
2. Identifier le bloqueur avec composer prohibits et composer depends. Demandez-vous: qu'est-ce qui empêche l'installation? Qui dépend de qui? L'arbre de dépendances (--tree) devient vite indispensable. Ne lancez aucune mise à jour avant d'avoir la réponse.
3. Vérifier vos contraintes dans composer.json. Toute contrainte sans borne supérieure est une bombe à retardement. Remplacez >=3.4 par ^3.4, figez les versions mineures qui vous importent, et assumez que la rigueur d'écriture est votre première ligne de défense.
4. Aligner l'environnement avec le code. PHP, extensions, bibliothèques système: la cohérence entre CI et production n'est pas un détail, c'est une condition de fiabilité. Si elles divergent, c'est là que le conflit naîtra.
5. Choisir la bonne option de mise à jour. -m par défaut pour limiter les surprises. -w quand vous savez exactement ce que vous voulez mettre à jour. -W seulement quand vous avez compris pourquoi l'option précédente ne suffit pas — et après avoir vérifié qu'elle ne crée pas de régression ailleurs.
6. Régénérer l'autoloader après correction. -o en production, jamais -a sans avoir pensé à APCu. Et mesurer avant d'annoncer un chiffre.
7. Documenter la cause racine. Pas dans un coin de Slack, dans votre dépôt. Un commentaire dans le commit, une entrée dans votre changelog, une note de run si vous en avez une. La prochaine personne qui héritera de votre code vous remerciera — et ce sera peut-être vous, six mois plus tard.
Ce que les recruteurs entendent vraiment
Je termine par un mot qui n'est pas technique, mais qui est sans doute le plus important de cet article. Quand un candidat me raconte un incident Composer en entretien, je ne lui demande pas la commande miracle qui a tout résolu. Je lui demande ce qu'il a compris. Parce qu'un conflit de dépendances en production n'est jamais qu'un symptème — d'un verrou mal géré, d'une contrainte trop permissive, d'un environnement qui diverge, ou d'une chaîne de déploiement qui manque de discipline.
Les entreprises qui recrutent des profils PHP expérimentés le savent. Elles ne cherchent pas des développeurs qui n'ont jamais vu d'incident — elles en cherchent qui en sortent plus lucides qu'ils n'y sont entrés. C'est cette posture, plus que n'importe quelle commande, qui fait la différence entre un candidat qu'on retient et un candidat qu'on oublie.
Alors la prochaine fois que composer install crache une erreur en production, respirez, ouvrez votre terminal, et rappelez-vous que derrière l'erreur se cache une histoire — et que c'est votre histoire de développeur qui est en train de s'écrire.




