jobsphp

Fichier composer.lock : pourquoi son rôle est crucial

Open Source & Outils. Fichier composer.lock : pourquoi son rôle est crucial

Deux développeurs qui clonent le même dépôt, qui installent la même version de Composer, et qui lancent composer install à quelques minutes d'intervalle peuvent se retrouver avec deux arbres de dépendances différents.

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.

EnvironnementSource de véritéCommande attendue
Poste développeurclone + lock versionnécomposer install
Pipeline CIcheckout + lockcomposer install (ou --no-dev sur un job prod)
Productiondéploiement du lock + vendorcomposer install --no-dev --no-interaction
Staging / previewlock versionnécomposer install

Trois règles opérationnelles qui découlent directement de ce tableau:

  • composer update ne 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é. Seul composer.lock l'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 un git pull, après un git checkout, dans la CI, sur un environnement vierge. Aucune décision à prendre.
  • composer update [paquet] — résolution à la demande. À exécuter quand composer.json change (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-lock dé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 comme lib-xml, lib-curl) satisfait les exigences figées dans le verrou, et non les valeurs éventuellement simulées par config.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 à chaque composer update intentionnel et qui se valide à chaque composer 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.

Questions fréquentes

Pourquoi mon application se comporte-t-elle différemment entre mon poste et la production ?
Cela arrive souvent lorsque le fichier composer.lock est absent ou ignoré, ce qui permet à Composer de résoudre les dépendances différemment selon le moment ou l'environnement.
Dois-je versionner le dossier vendor dans mon dépôt Git ?
Non, le répertoire vendor ne doit jamais être versionné. Seul le fichier composer.lock doit l'être pour permettre une régénération déterministe des dépendances.
Quelle est la différence entre composer install et composer update ?
composer install lit le fichier de verrouillage pour installer les versions figées, tandis que composer update interroge les dépôts pour mettre à jour les versions selon les contraintes du fichier composer.json.
Faut-il versionner composer.lock pour une bibliothèque PHP ?
Le verrou est optionnel pour une bibliothèque distribuée sur Packagist, car il n'influence que le projet racine et n'impose rien aux projets qui utilisent votre bibliothèque.
Comment vérifier si mes dépendances présentent des vulnérabilités ?
Vous devez utiliser la commande composer audit --locked, qui analyse les paquets référencés dans votre fichier de verrouillage pour détecter les failles de sécurité connues.