Composer updateでプロジェクトが壊れた話

何も変更していないのに動かなくなることは本当に起きる

ある日、特にコードを触っていないのにアプリケーションが動かなくなりました。
原因はシンプルです。composer updateを実行しただけでした。

冗談のようですが、PHP開発では珍しい話ではありません。
そして重要なのは、これはComposerのバグではないという点です。

結論から言うと、composer updateは「ライブラリを更新するコマンド」ではなく、
依存関係を再計算するコマンドです。
ここを誤解すると、同じことが何度でも起きます。

その日何が起きたのか

軽い気持ちで実行したコマンド

きっかけはよくあるものです。
セキュリティ修正の案内が来ました。

composer update

数分後、テストが通らなくなりました。
さらに本番環境でもエラーが発生しました。

変更したファイルは0です。
Gitの差分もありません。
しかしアプリケーションは壊れています。

実際に起きていたこと

Composerは、composer.jsonを元に依存関係を解決します。
ここで重要なのがバージョン指定です。

"require": {
  "framework/core": "^4.2"
}

この「^」は、4.xの最新まで許可する意味です。
つまり4.9が公開されれば、それも対象になります。

updateを実行すると次が起きます。

  • 直接依存が更新
  • 間接依存も更新
  • 依存の組み合わせが変わる

ここで問題が発生します。
互換性は「保証」ではなく「期待」だからです。

間接依存が最も危険

今回壊した原因は、自分が入れていないライブラリでした。
フレームワークが内部で使っているパッケージです。

開発者が意識していない部分ほど影響が大きくなります。

例:

  • ログライブラリの挙動変更
  • 日付処理の仕様変更
  • 例外クラスの変更

コードは触っていないのに挙動が変わります。
「昨日は通ったテスト」が突然落ちます。

なぜinstallでは起きないのか

ここで重要な違いがあります。

composer install

これはcomposer.lockを再現します。
つまり、以前と同じバージョンが入ります。

一方、

composer update

は、lockファイルを書き換えます。
依存関係の“新しい解”を求めます。

つまりupdateは修復ではなく再構築です。

よくある誤解:updateは安全なメンテナンスではない

多くの人が「とりあえずupdateしておくか」と考えます。
これはnpm感覚では自然です。

しかしComposerでは意味が違います。

  • update:構成変更
  • install:状態再現

この違いを知らないと、本番で事故が起きます。

実際の復旧手順

壊れたとき、最初にやるべきことは1つです。

git checkout composer.lock
composer install

これで元に戻る可能性が高いです。
つまりcomposer.lockはバックアップでもあります。

その後、原因を調べます。

composer why-not パッケージ名 バージョン

どの依存が衝突しているかが分かります。

なぜPHPでこの問題が目立つのか

PHPはフレームワーク依存が強い傾向があります。
そしてフレームワークは多数のライブラリに依存しています。

結果として、

> 1つのupdateが数十パッケージに影響する

という状況になります。
これはComposer特有というより、エコシステムの特徴です。

注意点:本番環境でupdateを実行しない

最も危険な行為があります。

  • 本番サーバーでcomposer update

これはデプロイではありません。
本番環境の再構築です。

環境差があると、開発と違う依存解決が行われる可能性もあります。
最悪の場合、再現不能バグになります。

まとめ:壊れたのはプロジェクトの前提だった

今回壊れた原因は、コードではありませんでした。
依存関係の前提条件です。

Composer updateは便利ですが、
それは「最新にしてくれる」からではありません。

> 依存関係を正しく表面化させるからです。

updateで壊れるプロジェクトは、実は以前から壊れる可能性を抱えていました。
たまたま固定されていただけです。

Composerは壊したのではなく、
隠れていた前提を露出させただけだった、というのが実感に近いです。