Зачем нужен composer.lock

composer.json описывает намерение: «проекту нужен monolog/monolog версии ^3.0, PHP ^8.3, расширение ext-pdo». composer.lock фиксирует результат вычисления: какие именно версии пакетов были выбраны, с какими зависимостями, из каких источников и с какими metadata.

Это главное различие. composer.json — диапазоны и правила. composer.lock — снимок конкретного dependency graph. Поэтому для приложений lock-файл обычно коммитят. CI, staging, production и машина разработчика должны ставить один и тот же набор пакетов, а не «самые свежие совместимые на утро деплоя».

Для библиотек правило мягче: lock-файл может быть полезен для локальных тестов, но потребитель библиотеки всё равно решает зависимости в своём приложении. В приложении же отсутствие lock-файла почти всегда делает сборку менее воспроизводимой.

flowchart TD A[composer.json: constraints] --> B{Команда} B -->|composer update| C[Решение dependency graph] C --> D[Новый composer.lock] D --> E[vendor/] B -->|composer install + lock есть| F[Чтение точных версий из composer.lock] F --> E E --> G[autoload.php] G --> H[Приложение / CI / production] H --> I[composer audit] H --> J[check-platform-reqs]
flowchart TD
    A[composer.json: constraints] --> B{Команда}
    B -->|composer update| C[Решение dependency graph]
    C --> D[Новый composer.lock]
    D --> E[vendor/]
    B -->|composer install + lock есть| F[Чтение точных версий из composer.lock]
    F --> E
    E --> G[autoload.php]
    G --> H[Приложение / CI / production]
    H --> I[composer audit]
    H --> J[check-platform-reqs]
`update` меняет lock-файл, а `install` восстанавливает зависимости из уже зафиксированного lock-файла.
Быстрое повторение
Почему для приложения обычно коммитят `composer.lock`, а не полагаются только на `composer.json`?

install: поставить зафиксированное

composer install — команда для восстановления зависимостей проекта. Если рядом есть composer.lock, Composer берёт точные версии из него и устанавливает их в vendor/.

composer install

Типичный сценарий:

git pull
composer install

После git pull lock-файл мог измениться: кто-то добавил пакет, обновил Symfony-компонент, убрал устаревшую зависимость. composer install приводит локальный vendor/ к состоянию, которое описано в lock-файле.

Если composer.lock отсутствует, install вынужден решить зависимости заново и создать lock-файл. Это нормально для первого локального запуска нового проекта, но плохо как production-паттерн: на сервере вы хотите не «решить зависимости сейчас», а поставить уже проверенный набор.

Для production обычно используют более жёсткую форму:

composer install --no-dev --optimize-autoloader --no-interaction

--no-dev не ставит пакеты из require-dev и не включает autoload-dev. То есть phpunit/phpunit, phpstan/phpstan, тестовые namespace и dev-only утилиты не попадают в runtime. Это продолжает разделение из Composer, Packagist и composer.json: если код нужен на production, он должен быть в require, а не в require-dev.

--optimize-autoloader строит более быструю classmap для автозагрузчика. Это особенно уместно на production, где файлы не меняются при каждом запросе. В dev-среде оно часто не нужно: важнее быстрый цикл правки кода.

Быстрое повторение
Что сделает `composer install`, если рядом уже есть `composer.lock`?

update: пересчитать версии

composer update делает другое: он заново решает dependency graph по constraints из composer.json, записывает новые конкретные версии в composer.lock и затем ставит их.

composer update

Это команда изменения зависимостей, а не обычная команда деплоя. Если запустить её без аргументов, Composer может обновить много пакетов сразу: прямые зависимости, транзитивные зависимости, иногда целый пласт экосистемы. Такой diff сложнее ревьюить и сложнее откатывать.

Для аккуратного обновления чаще указывают пакет явно:

composer update monolog/monolog

Так проще понять, почему изменился lock-файл. В merge request обычно смотрят не только composer.json, но и composer.lock: какие пакеты реально поменялись, не приехал ли неожиданный major, не заменился ли source URL, не появилась ли новая цепочка зависимостей.

Короткое практическое правило:

composer install  = довериться composer.lock
composer update   = изменить composer.lock

Если на production запускают composer update, production становится местом принятия dependency-решений. Это почти всегда неверная граница ответственности. Обновления должны проходить локальные проверки, CI, линтеры и автоматические проверки, ревью lock-файла и только потом деплой.

Быстрое повторение
`composer update` — это команда для обычного деплоя или для изменения зависимостей? Почему?

Reproducible installs и vendor/

vendor/ обычно не коммитят. Его можно восстановить из composer.json + composer.lock, поэтому Git-репозиторий остаётся компактным, а сторонний код не смешивается с вашим.

Воспроизводимость здесь не абсолютная магия, но сильная гарантия: при одинаковом lock-файле Composer должен поставить одинаковые версии пакетов. Это снижает класс ошибок «у меня работает, а на сервере другой patch-релиз». Особенно это важно перед миграциями PHP-версии, где одна транзитивная зависимость может поддерживать PHP 8.4, а другая ещё нет. Поэтому тема напрямую связана с Миграции между версиями PHP.

Плохой diff:

modified: composer.json
modified: composer.lock
modified: vendor/symfony/console/Application.php
modified: vendor/psr/log/LoggerInterface.php

Нормальный diff для приложения:

modified: composer.json
modified: composer.lock

vendor/ пересоберёт CI или deploy pipeline.

Platform requirements: PHP, extensions, system libs

Composer проверяет не только пакеты, но и platform packages: php, ext-mbstring, ext-pdo, ext-json, lib-*. Эти требования не скачиваются Composer’ом; они должны быть установлены в окружении.

{
  "require": {
    "php": "^8.3",
    "ext-pdo": "*",
    "ext-mbstring": "*"
  }
}

Если расширение используется кодом, его стоит явно указать. Иначе локально всё может работать просто потому, что расширение уже установлено, а контейнер или production-сервер упадёт позже.

Есть настройка config.platform, которая позволяет зафиксировать целевую платформу для dependency resolution:

{
  "config": {
    "platform": {
      "php": "8.3.0"
    }
  }
}

Это полезно, если локально у разработчика PHP 8.5, а production ещё на 8.3: Composer не выберет пакет, которому нужен PHP выше целевой версии. Но это не заменяет проверку реального сервера. Для неё есть отдельная команда:

composer check-platform-reqs

Она сверяет требования установленных пакетов с настоящим PHP и расширениями текущего окружения, а не только с тем, что указано в config.platform.

Флаги вроде --ignore-platform-reqs или --ignore-platform-req=ext-foo стоит считать временным обходом, а не нормой. Они могут помочь собрать проект в неполном локальном окружении, но если так скрыть отсутствие расширения на production, ошибка просто переедет из Composer в runtime.

composer audit: проверка известных проблем

composer audit проверяет установленные или зафиксированные пакеты по security advisories и dependency policies. По умолчанию Composer использует Packagist API, если проект не настроил другие репозитории.

composer audit
composer audit --locked
composer audit --no-dev
composer audit --format=json

--locked полезен в CI: проверяется то, что записано в lock-файле, независимо от текущего состояния vendor/. --no-dev исключает dev-зависимости, если вы хотите отдельно оценить production surface. JSON-формат удобен для CI-отчётов и security dashboards.

Важно не путать audit с полным security review. Он находит известные advisory по зависимостям, но не доказывает, что приложение безопасно. Он не заменяет темы Prepared statements и SQL injection, XSS, экранирование вывода и шаблоны, CSRF и state-changing запросы и Конфигурация безопасности PHP. Audit отвечает на более узкий вопрос: «есть ли в нашем dependency graph известные проблемные версии?»

Если advisory действительно не применим, его можно игнорировать с причиной в конфиге. Но игнор без причины быстро превращается в мусорный список. Хорошая запись объясняет, почему риск принят, когда вернуться к вопросу и какой mitigation уже есть.

Практичный pipeline команд

Для локальной разработки:

composer install
composer validate
composer audit
composer check

Для обновления одной зависимости:

composer update vendor/package
composer audit --locked
composer check

Для production-сборки:

composer install --no-dev --optimize-autoloader --no-interaction
composer check-platform-reqs

Смысл не в том, чтобы запомнить набор флагов как заклинание. Граница простая: install воспроизводит уже принятое решение, update принимает новое dependency-решение, audit подсвечивает известные риски, platform checks сверяют ожидания Composer с реальным PHP-окружением.

См. также

Источники

  1. Composer docs: Basic usage
  2. Composer docs: Command-line interface / Commands
  3. Composer docs: Config