Un paquet publié entre-temps, une contrainte relue, un miroir régional qui renvoie une version plus récente — il suffit de peu pour que la résolution diverge. Sur une application en production, ce n'est pas une vue de l'esprit: c'est un mode de défaillance silencieux qui attend la prochaine montée de version ou le prochain basculement d'infrastructure pour se matérialiser.
Le fichier qui rend ce comportement maîtrisable s'appelle composer.lock. Sa taille varie d'un projet à l'autre selon la profondeur de l'arbre de dépendances — d'un fichier compact pour un projet léger à un volume bien plus important sur un monolithe framework ou un CMS sur mesure — mais son rôle reste le même: figer ce que Composer a effectivement résolu.
composer.json déclare, composer.lock enregistre
composer.json exprime des contraintes: ^2.6, ~1.3, >=8.1, dev-main, 8.1.*. Il décrit ce que le projet veut. composer.lock enregistre ce que Composer a effectivement résolu: nom, version exacte, hash, URL de source et liens entre paquets. En pratique, le fichier est un JSON structuré autour de quelques clés standard: un content-hash qui détecte les désynchronisations entre manifeste et verrou, un bloc packages listant les versions résolues avec leur source et leur hash, un bloc packages-dev pour les dépendances de développement, plus quelques métadonnées internes. Ce content-hash est précisément ce qui permet à Composer de signaler, pendant install, que composer.json a été modifié sans que le verrou soit régénéré en cohérence — une alerte typique du genre The lock file is not up to date with the latest changes in composer.json.
Concrètement, à chaque composer update, Composer:
- lit les contraintes dans
composer.json; - interroge les dépôts configurés (Packagist, dépôts privés,
path,vcs); - choisit un jeu de versions qui satisfait l'ensemble des contraintes;
- réécrit
composer.lock.
À l'inverse, composer install ne relance pas la résolution si le verrou existe: il lit les versions figées et les installe telles quelles. Sans verrou, en revanche, install se comporte comme un update et résout à partir du seul composer.json, ce qui explique les divergences silencieuses évoquées plus haut.
Le verrou n'est pas un fichier de cache, c'est un contrat de versions: il aligne les versions résolues entre toutes les machines qui partagent le même commit Git, sans prétendre à une portabilité absolue.
En l'absence du lock, deux développeurs tirant le même composer.json à quelques minutes d'intervalle peuvent obtenir deux graphes de dépendances différents si un paquet est publié entre-temps — patch de sécurité, release correctrice, hotfix d'un mainteneur. Pour un projet à cycle de release court ou avec des contraintes lâches (*, dev-main, ^0.x), l'écart est suffisant pour faire diverger les comportements en CI, en staging ou en prod, sans qu'aucun commit individuel n'y apparaisse comme responsable.
Reproductibilité: ce que verrouille vraiment composer.lock
Le verrou a une fonction claire: aligner les installations à travers les environnements. Pas un, pas deux, mais tous les contextes où tourne l'application — à condition que le verrou et la plateforme restent cohérents entre eux.
| Environnement | Source de vérité | Commande attendue |
|---|---|---|
| Poste développeur | clone + lock versionné | composer install |
| Pipeline CI | checkout + lock | composer install (ou --no-dev sur un job prod) |
| Production | déploiement du lock + vendor | composer install --no-dev --no-interaction |
| Staging / preview | lock versionné | composer install |
Trois règles opérationnelles qui découlent directement de ce tableau:
composer updatene se lance jamais sur un environnement de production. La résolution est faite une fois, en local ou en CI, et le résultat est validé avant déploiement.- Le répertoire
vendor/n'est jamais versionné. Seulcomposer.lockl'est.vendor/se régénère à partir du verrou à chaque installation propre — cette séparation est ce qui rend les builds déterministes. - Si le lock change, l'environnement change. Toute mise à jour de dépendance non triviale mérite un changelog, idéalement un test de non-régression dédié, et au minimum un build vert sur l'ensemble des cibles de la matrice (PHP 8.1, 8.2, 8.3, 8.4 si la matrice le prévoit).
Pour les projets open source multi-mainteneurs, c'est précisément ce mécanisme qui rend les builds alignés: un job GitHub Actions qui installe strictement les versions du verrou reproduit le même arbre qu'un poste contributeur qui vient de forker le dépôt — à condition que la plateforme PHP et les extensions soient elles-mêmes alignées.
Cycle de vie: install, update, validate, audit
La frontière entre install et update est souvent mal comprise. Quatre opérations couvrent l'essentiel du cycle de maintenance:
composer install— usage quotidien. Lit le verrou, installe, s'arrête. C'est ce qu'on lance après ungit pull, après ungit checkout, dans la CI, sur un environnement vierge. Aucune décision à prendre.composer update [paquet]— résolution à la demande. À exécuter quandcomposer.jsonchange (nouvelle dépendance, relâchement de contrainte, mise à jour manuelle d'une borne) ou pour tirer intentionnellement les dernières versions compatibles. Toujours en local ou en CI, jamais en direct sur un serveur en production.composer validate— sanity check. Vérifie la syntaxe du manifeste et, si le verrou existe, sa cohérence avec ce dernier. L'option--no-check-lockdésactive explicitement ce second contrôle, ce qui est rarement une bonne idée et n'a aucune justification en pre-commit. La recommandation Composer est de l'exécuter avant chaque commit et avant chaque tag de release.composer check-platform-reqs --lock— diagnostic plateforme. Vérifie que la plateforme réelle (version PHP, extensions chargées, bibliothèques système commelib-xml,lib-curl) satisfait les exigences figées dans le verrou, et non les valeurs éventuellement simulées parconfig.platform. C'est l'outil à exécuter sur le runner cible avant un déploiement.
Le verrou n'est pas un fichier qu'on fige pour l'éternité. C'est un snapshot daté qui se régénère à chaquecomposer updateintentionnel et qui se valide à chaquecomposer validate.
Un point de comportement à connaître, introduit dans les versions récentes de Composer: une dépendance requise déclarée dans composer.json mais absente de composer.lock peut désormais provoquer une erreur d'install plutôt qu'un simple avertissement. Pour les équipes qui lockaient par habitude sans jamais régénérer après ajout d'une dépendance, c'est un signal utile: un verrou incomplet a plus de chances d'être détecté en CI qu'auparavant, et la résolution ne s'exécute plus en arrière-plan sans crier gare.
Application finale vs bibliothèque: deux conventions distinctes
Composer distingue explicitement les deux cas, et c'est là que beaucoup d'équipes se trompent en recopiant une convention sans la comprendre.
Application finale (SaaS, API publique, site Drupal ou WordPress sur mesure, monolithe Symfony, Laravel app): composer.lock doit être versionné. C'est la recommandation officielle, et c'est non négociable dès qu'on a plus d'un développeur ou un pipeline automatisé. Le lock devient la source de vérité partagée: tout le monde tire la même résolution, un build qui rate en CI reproduit exactement l'environnement du développeur qui a committé la mise à jour, et un rollback applicatif reste un git revert propre.
Bibliothèque Composer distribuée sur Packagist (un package qu'on composer require depuis un autre projet): le verrou est optionnel. Il peut rester committé pour stabiliser les tests internes de l'équipe et garantir la reproductibilité de la suite de tests, mais il n'influence pas les dépendances résolues dans les projets consommateurs: le verrou ne s'applique qu'au projet racine. Une lib qui versionne son lock n'impose rien en aval; une lib qui l'ignore non plus, fonctionnellement parlant.
Le réflexe à proscrire absolument: importer la convention « j'ignore composer.lock partout » d'un projet de bibliothèque vers une application de production, ou importer « on commit le lock partout » dans une lib où ça ne sert fonctionnellement à rien d'autre qu'à figer les tests internes. La règle se déduit du type de projet, pas de l'historique de l'équipe.
Pour neutraliser complètement le verrou — cas rare, utile uniquement pour des outils CLI jetables ou des projets de démo — la directive config.lock positionnée à false dans composer.json désactive la génération et l'utilisation du lock. Composer ignore alors tout composer.lock existant. C'est une option à connaître, pas à utiliser par défaut.
Audit, plateforme et sécurité: ce que le verrou ne couvre pas
Trois angles où composer.lock peut donner une fausse impression de sécurité.
Audit des vulnérabilités. composer audit --locked audite les paquets référencés dans le verrou, indépendamment de l'état courant du répertoire vendor/. C'est la commande à brancher en CI sur tout projet qui prend la sécurité au sérieux. Formats de sortie documentés: table, plain, json, summary — le format json se pipe directement vers un outil tiers, un dashboard interne ou un dépôt d'artefacts. Le verrou fige des versions, y compris potentiellement des versions vulnérables: sans audit périodique, le contrat de versions devient un contrat d'exposition. Cas typique, observé sur des dizaines de projets open source: une dépendance transitivement requise en ^1.0 est figée dans le lock sur 1.0.0. Une CVE sort, un patch 1.0.3 est publié le lendemain, mais le verrou continue de pointer sur la version vulnérable jusqu'à la prochaine mise à jour intentionnelle, qui peut ne jamais arriver. Sans composer audit --locked dans la pipeline, l'incident dort.
Cohérence plateforme. Le verrou enregistre ce qui a été résolu, pas ce qui est réellement disponible sur le serveur cible. composer check-platform-reqs --lock est l'outil à coupler avec le déploiement: il lit le verrou, confronte les exigences php, ext-*, lib-*, composer-plugin-api, composer-runtime-api à la plateforme d'exécution, et ignore les éventuelles valeurs simulées par config.platform. La directive config.platform-check accepte trois valeurs: php-only (défaut), true (ajoute le contrôle des extensions et libs système), false (désactive tout contrôle). Sur un build de production, true est le minimum raisonnable. Un composer install --no-dev qui passe en CI mais échoue en prod signale presque toujours un écart de plateforme, pas une erreur de lock — d'où l'importance de garder strictement la même image PHP et les mêmes extensions entre les deux contextes.
Mise à jour contrôlée des dépendances. Tout patch de sécurité, ou plus largement toute montée intentionnelle, suit le même rituel:
1. Mise à jour de la borne dans composer.json (ou via composer require vendor/package:^2.3).
2. composer update local + lecture attentive du diff de composer.lock.
3. Tests + composer audit --locked.
4. Commit du lock mis à jour.
5. composer validate en pre-commit et en CI.
6. Déploiement par composer install strictement, sans update.
Une étape sautée et la chaîne d'alignement casse. Un update accidentel sur un environnement de prod réécrit le lock à l'insu de l'équipe et annule tout le travail de revue fait en amont.
Verdict
Application PHP en production: composer.lock versionné, composer install partout sauf sur la machine où la résolution est faite, composer validate en pre-commit, composer audit --locked en CI sur chaque PR, composer check-platform-reqs --lock avant chaque déploiement. C'est la chaîne standard pour quiconque déploie en prod.
Bibliothèque Composer distribuée: lock optionnel, committé uniquement s'il stabilise les tests internes. Pas d'impact en aval dans les deux cas.
Cas limite à neutraliser explicitement: pour les outils CLI éphémères ou les démos jetables, config.lock: false désactive proprement le mécanisme.
Composer 2.5 puis 2.9.2 ont successivement durci les règles autour de ce fichier — alignement renforcé sur les versions résolues, contrôles plus stricts en cas d'écart entre manifest et verrou. Le verrou n'est pas une garantie absolue de sécurité ou de portabilité: il fige un instantané qu'il faut ensuite auditer, valider et régénérer. Mais c'est le contrat de versions outillé nativement dans l'écosystème PHP, et les projets qui s'en passent volontairement restent l'exception plutôt que la règle.
