У Composer два файли з різними ролями. composer.json — це декларація намірів: «мені підійде будь-який Laravel 12.x». composer.lock — протокол результату: точні версії всіх пакетів, включно з транзитивними, які ви ніколи не згадували, плюс хеш коміту кожного з них, посилання на дистрибутив і content-hash — відбиток тих секцій composer.json, що впливають на резолюцію. Саме тому lock зазвичай на кілька тисяч рядків: у ньому не десяток ваших залежностей, а все дерево цілком.
composer install за наявності lock не виконує резолву взагалі — просто розпаковує перелічені версії. Це швидко, детерміновано і дає однаковий vendor/ на ноутбуці, в CI і на сервері. composer update навпаки: ігнорує зафіксовані версії, звіряється з composer.json, витягує з packagist найновіше, що вкладається в діапазони, розв'язує конфлікти між залежностями залежностей і переписує lock. Якщо composer.json змінили, а lock ні, install виведе попередження про невідповідність content-hash і все одно поставить старе; у CI цю розбіжність ловлять командою composer validate --strict, яка на ній падає.
Діапазони записують семантичним версіонуванням: MAJOR.MINOR.PATCH, де мажор означає несумісні зміни, мінор — сумісні доповнення, патч — виправлення. Практично використовують два оператори. Каретка ^1.2.3 дозволяє все до наступного мажора: >=1.2.3 <2.0.0. Тильда ~1.2.3 піднімає лише останню вказану позицію: >=1.2.3 <1.3.0, а от ~1.2 — це вже >=1.2 <2.0. Окремо треба знати виняток для нульових мажорів: у версіях 0.x Composer вважає ламким лівий ненульовий сегмент, тому ^0.3.1 означає >=0.3.1 <0.4.0, а не «до 1.0». Composer не читає код пакета — він довіряє номеру, тож захист від автора, який зламав API в мінорному релізі, лише один: зафіксований lock і тести після кожного оновлення.
Звідси робочий процес. Lock комітять у репозиторій (для застосунку — обов'язково; у бібліотеці він ігнорується споживачами, але корисний для власного CI). Оновлюють локально й точково: composer update vendor/package -W, потім тести, потім коміт зміненого lock окремою гілкою — так у код-рев'ю видно, що саме змінилося у версіях. composer require робить те саме автоматично, тобто частковий update лише для нового пакета. На деплої запускають виключно composer install --no-dev --optimize-autoloader: --no-dev викидає PHPUnit і решту інструментів розробки, --optimize-autoloader будує статичну мапу класів замість пошуку файлів на диску.
Межа цього підходу в тому, що lock фіксує версії, але не гарантує їхньої безпеки: зафіксований на пів року пакет може мати опубліковану вразливість. Тому в пайплайні тримають composer audit (доступний з Composer 2.4), який звіряє lock з базою відомих CVE, а оновлення роблять регулярними невеликими порціями, а не одним стрибком через два мажори раз на рік. Другий нюанс — платформа: якщо на ноутбуці PHP 8.4, а на сервері 8.2, резолюція локально дозволить те, що на сервері не встановиться. Лікується це config.platform.php у composer.json, а не прапорцем --ignore-platform-reqs, який просто вимикає перевірку й переносить падіння на продакшен.
# composer.json — діапазони, тобто наші наміри
# "laravel/framework": "^12.0" → >=12.0.0 <13.0.0 (мажор фіксуємо)
# "nesbot/carbon": "~3.8.0" → >=3.8.0 <3.9.0 (патчі, без мінорів)
# "league/csv": "^0.9.2" → >=0.9.2 <0.10.0 (нульовий мажор: ламає мінор)
# 1. Розробник: ставимо строго те, що у lock. Резолву немає, тому швидко.
composer install
# 2. Оновлення — тільки локально, у гілці, точково.
composer outdated --direct # що взагалі має новіші версії
composer update nesbot/carbon -W # -W = разом із залежностями цього пакета
vendor/bin/pest # тести на новому наборі версій
git add composer.json composer.lock && git commit -m "Bump carbon"
# 3. Конфлікт у composer.lock після мержу: файл не редагують руками.
git checkout --ours composer.json
git checkout --ours composer.lock
composer update --lock # перерахувати content-hash без зміни версій
# 4. Деплой: install і нічого більше. update тут = нетестований код у релізі.
composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
composer audit # відомі CVE у зафіксованих версіях
Скажіть одним реченням: «composer.json — це те, що ми дозволяємо, composer.lock — це те, що ми перевірили». Далі додайте, що update робиться локально в окремій гілці з тестами, а на сервері живе тільки install — це відповідь рівня людини, яка деплоїла, а не читала документацію.