はじめに
cloneしたらcomposer install、依存を足したいときはcomposer require、たまにcomposer update。この程度の感覚でComposerを使っていました。
それぞれが何をしているのかいまいち理解していなかったので整理しました。
環境はDockerのphp:8.4-cli(8.4.23)とComposer 2.10.2です。載せている出力は手元で出たものです。
各コマンドが実施していること
| コマンド | やること | composer.json | composer.lock | vendor/ |
|---|---|---|---|---|
composer install |
lockのとおりに入れる | 触らない | 触らない(無ければ書く) | 作る |
composer update |
条件を読み直して答えを出し直す | 触らない | 書き換える | 更新する |
composer require |
依存を追加する | 書き換える | 書き換える | 更新する |
composer install
lockの通りインストールします。
vendor/を消してから打つと、こう出ます。
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 2 installs, 0 updates, 0 removals
- Installing psr/log (3.0.2): Extracting archive
- Installing monolog/monolog (3.10.0): Extracting archive
Generating autoload files
from lock fileとある通り、どのバージョンを入れるかをcomposer.jsonではなくcomposer.lockから読んでいます。2行目でlockの中身が今の環境で動くか検証し、最後にvendor/autoload.php一式を作っています。
lockに答えが書いてあるので、どのバージョンを入れるかを考える処理は動きません。解決のためのパッケージ情報も取りに行かないので、キャッシュが効いていればネットワークを切っても通ります。
$ COMPOSER_DISABLE_NETWORK=1 composer install
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 2 installs, 0 updates, 0 removals
- Installing psr/log (3.0.2): Extracting archive
- Installing monolog/monolog (3.10.0): Extracting archive
Generating autoload files
lockが古くても書き換えはしない
monolog/monologの制約を^3.10から^3.0に緩めてからinstallを打つと、警告は出ますがcomposer.lockは書き換わりませんでした。
Verifying lock file contents can be installed on current platform.
Warning: The lock file is not up to date with the latest changes in composer.json.
You may be getting outdated dependencies. It is recommended that you run
`composer update` or `composer update <package name>`.
Nothing to install, update or remove
installはあくまでlockのとおりに入れるだけで、条件を読み直すことはしない、ということでした。lockを追随させたければupdateを打つ必要があります。
警告で済んだのは、既にlockに載っているパッケージの制約をいじったからでした。lockに無いパッケージを足してからinstallを打つと、同じ警告が出たうえで、さらにエラーになって止まります。
Warning: The lock file is not up to date with the latest changes in composer.json.
...
- Required package "psr/container" is not present in the lock file.
This usually happens when composer files are incorrectly merged or the
composer.json file is manually edited.
composer.jsonに依存を書き足してinstall、という手順は通りません。
lockが無いと、installはinstallにならない
そのlockが無い状態でcomposer installを打つと、1行目でこう言われます。
No composer.lock file present. Updating dependencies to latest instead of
installing from lock file.
lockが無いのでupdateします、と書いてあります。打ったのはinstallでも、走るのはupdateの処理でした。
そうなると下記のようなscriptsを書いている場合、意図しない挙動になるかもしれないです。
"scripts": {
"post-install-cmd": ["@php scripts/hello.php"],
"post-update-cmd": ["@php scripts/updated.php"]
}
lockがない状態からinstallを打つと、呼ばれたのはpost-update-cmdの方でした。
Generating autoload files
> @php scripts/updated.php
post-update-cmd が呼ばれた
lockができた状態で打ち直すと、今度はpost-install-cmdが呼ばれます。post-install-cmdを書いたのに動かない、という事故はここが原因になりえます。
(余談)npmの感覚だと引っかかるところ
npmはnpm installがpackage-lock.jsonを書くので、installがlockを作るのが自然に感じます。Composerはlockが無いと「まだ答えが確定していない」と判断してupdateに倒れる、という違いでした。名前が同じでも役割は違っています。
composer update
同じプロジェクトでcomposer updateを打つと、出力が変わります。
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Writing lock file
Installing dependencies from lock file (including require-dev)
Nothing to install, update or remove
Generating autoload files
No security vulnerability advisories found.
installには無かった行が3つあります。
| 行 | やっていること |
|---|---|
Loading composer repositories with package information |
Packagistからパッケージ情報を取得 |
Updating dependencies |
条件を満たすバージョンの組み合わせを計算 |
Writing lock file |
結果をcomposer.lockに書き出す |
そのあとにInstalling dependencies from lock fileが続いているのが分かりやすいところで、updateは解決してlockを書いてからinstallしているという構造でした。installはその後半だけを実行していることになります。
No security vulnerability advisories found.も出ています。updateのときは脆弱性のチェックも走るようです。
パッケージ情報はどこから取ってくるのか
Loading composer repositoriesで何を取りに行っているのか、-vvvを付けると見えます。
composer update -vvv
guzzlehttp/guzzleを^8.0で1つだけ書いたプロジェクトで、取得したURLを抜き出すとこうなりました。
https://repo.packagist.org/packages.json
https://repo.packagist.org/p2/guzzlehttp/guzzle.json
https://repo.packagist.org/p2/guzzlehttp/promises.json
https://repo.packagist.org/p2/guzzlehttp/psr7.json
https://repo.packagist.org/p2/psr/http-client.json
https://repo.packagist.org/p2/psr/http-factory.json
https://repo.packagist.org/p2/psr/http-message.json
https://repo.packagist.org/p2/symfony/polyfill-php80.json
https://repo.packagist.org/p2/symfony/polyfill-php82.json
最初のpackages.jsonだけ性格が違って、この中に取得先のテンプレートが書かれていました。
$ php -r '$d = json_decode(file_get_contents("https://repo.packagist.org/packages.json"), true); echo $d["metadata-url"];'
https://repo.packagist.org/p2/%package%.json
%package%をパッケージ名に差し替えて、必要なものだけを順に取りに行く仕組みです。URLが直接書かれているわけではないので、repositoriesを書けば取得先を変えられます。
この件数は制約の緩さで変わります。同じguzzleでも^8.0をやめて*にすると44件になりました。doctrine/annotationsやsymfony/validatorまで出てくるのですが、これはguzzleの古い版が候補に残るからで、その時代の依存も読みに行くことになります。
取得したものはどこに置かれるのか
取得したものはキャッシュされます。場所は設定から引けます。
$ composer config --global cache-dir
/root/.composer/cache
$ ls /root/.composer/cache
files
repo
コンテナのrootで動かしているのでこのパスですが、場所は環境によって変わります。XDG_CONFIG_HOMEのようなXDG系の環境変数が定義されていると~/.cache/composerになりました。
repo/にパッケージ情報、files/にダウンロードしたパッケージ本体が入っていました。
$ du -sh /root/.composer/cache/*
748K /root/.composer/cache/files
2.1M /root/.composer/cache/repo
一度取ったものは残るので、同じバージョンを入れ直すときに再ダウンロードは起きません。vendor/を消してからinstallし直しても、出力にDownloadingは1件も出ませんでした。おかしくなったらcomposer clear-cacheで消せます。
最新版が選ばれるとは限らない
updateは最新にするコマンドだと思っていたのですが、実際は条件を全部満たす組み合わせを探すコマンドでした。
guzzleのバージョンを指定せず、psr/http-messageだけ^1.0に縛ってみます。
{
"require": {
"guzzlehttp/guzzle": "*",
"psr/http-message": "^1.0"
}
}
guzzlehttp/guzzle 7.15.2 ← 最新は 8.0.1
guzzlehttp/psr7 2.13.0
psr/http-message 1.1
guzzleの最新8.0.1は内部でguzzlehttp/psr7の3系を使い、そのpsr7はpsr/http-messageの2系を必要とします。こちらが^1.0と書いたので両立しません。そこでguzzle自体を7系まで下げて、成立する組み合わせを持ってきた、という結果でした。
古いものが入って困ったときに疑うのは、まず自分が書いた制約になります。理由はcomposer why-notで聞けます。
composer why-not guzzlehttp/guzzle 8.0
composer require
composer require monolog/monologを打つと、最初の2行がこれでした。
./composer.json has been updated
Running composer update monolog/monolog
composer.jsonに書いてからupdateを呼んでいます。requireの実体は、対象を絞ったupdateでした。以降はupdateと同じ流れになります。
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
- Locking monolog/monolog (3.10.0)
- Locking psr/log (3.0.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing psr/log (3.0.2): Extracting archive
- Installing monolog/monolog (3.10.0): Extracting archive
Generating autoload files
Using version ^3.10 for monolog/monolog
違うのは最後の行です。バージョンを指定せずにrequireすると、Composerが最新版を調べて制約を決めてくれます。
// 打つ前
{ "name": "example/app", "require": {} }
// 打った後
{ "name": "example/app", "require": { "monolog/monolog": "^3.10" } }
手でcomposer.jsonに書いてからcomposer update monolog/monologを打つのと結果は同じですが、requireなら制約の書き方を考えずに済みます。パッケージ名を付けずにcomposer updateを打つと、頼んでいないパッケージまで解き直されて一緒に上がるので、そこは別物でした。開発時だけ必要なものは--devを付けるとrequire-devに入ります。
composer require --dev phpunit/phpunit
(余談)失敗するとcomposer.jsonは元に戻る
composer.jsonに書くのと解決するのはどちらが先なのか気になったので、解決できない指定を渡してみました。
composer require "monolog/monolog:^99.0"
./composer.json has been updated
Running composer update monolog/monolog
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
...
Installation failed, reverting ./composer.json and ./composer.lock to their
original content.
./composer.json has been updatedまで進んでから失敗し、最後に戻したと自分で言っています。先に書いたうえで、失敗したらロールバックする作りでした。戻すのはcomposer.jsonだけでなくcomposer.lockもです。
おわりに
各コマンドが何をしているのかをまとめてみました。
composer update = 情報を取る → 解決する → lockを書く → 配置する → autoload生成
composer install = lockを読む → 配置する → autoload生成
composer require = composer.jsonに追記 → 対象を絞ったupdate
installはupdateの後半だけを実行しているから速く、lockがあるから何度打っても同じ組み合わせが入る、というところに落ち着きました。