この記事は、PHP - Qiita Advent Calendar 2025 のDay13です。
ちょうど1ヵ月前に、Composerは新バージョン2.9をリリースしました。
https://blog.packagist.com/composer-2-9/
革新的な機能!というものが登場したバージョンでは無いかも知れませんが、日常のちょっとした場面で「明らかに便利」と思えるような、痒いところに手が届く系の機能追加が行われています。
content-hashのコンフリクトへの対処が楽になる
その中でも、個人的なお気に入りが「Automatic Lock File Conflict Recovery」です。
リリースノートでは、次のように紹介されています(翻訳は、筆者によるDeepLでの機械翻訳による)
自動ロックファイル競合回復
Composerは更新時に発生する単純なロックファイル競合を自動的に回復するようになりました。content-hashプロパティのみが競合している場合、
update --lockを実行するか、特定のパッケージの更新を再適用すると、git競合マーカーを無視してロックファイルを読み込みます。
より具体的には?
Pull Requestのディスクリプションで説明されているシナリオが分かりやすいです。
(PRはコチラ: https://github.com/composer/composer/pull/11517)
意訳・編集を加えながら紹介します。
before/after
今までは、content-hashがコンフリクトした際には次のようなステップで対処をしていました。
-
git merge ...で、composer.lockにマージコンフリクトがマークされる -
git diff composer.lockで内容を見てみると、content-hashフィールドだけがコンフリクトしていることが分かる -
【大事なポイント】👉️
この状態で、composer update --lockを実行しても上手く行かない。composer.lockが正常なJSON構文になっていないため、Composerがパースをする段階で例外を吐いて処理が停止される1 -
composer.lockをエディタで開く- 該当する行を削除して、マージコンフリクトの解消を行う
- 必須ではないが、
content-hashを意図的におかしな値に書き換える(例えばx)。こうすることで、「たまたま適切なhashだった」を回避して、Composerに後続のステップを確実に実施させられる - 編集結果を保存し、エディタを閉じる
-
git diff composer.lockを実行して、マージコンフリクトが解消されていることを確認する -
composer update --lockを実行して、正しいcontent-hashを生成。要求するパッケージがインストール可能になっていることも併せて確かめる -
git diff composer.lockを実行して、content-hashがbase/targetブランチの両辺から更新されていることを確認する -
git add composer.lockを実行 -
git commitやgit merge --continue、git rebase --continueを実行し、作業を進める
なかなか面倒くさいですよね。
作業がかなり定型的なのに、チマチマとしたステップがいくつもあるので、ここは"怠惰”にいきたいところ。
それでは、2.9からはどうなるのでしょう?
-
git merge ...で、composer.lockにマージコンフリクトがマークされる -
git diff composer.lockで内容を見てみると、content-hashフィールドだけがコンフリクトしていることが分かる - ココは不要に
- ココは不要に
- ココは不要に
-
composer update --lockを実行して、正しいcontent-hashを生成。要求するパッケージがインストール可能になっていることも併せて確かめる -
git diff composer.lockを実行して、content-hashがbase/targetブランチの両辺から更新されていることを確認する -
git add composer.lockを実行 -
git commitやgit merge --continue、git rebase --continueを実行し、作業を進める
素晴らしい👏
いい感じに「チマチマした部分」がなくなりました。嬉しいですね!
どうやってるの?
折角なので、実現方法も見てみましょう。
実行は composer update --lock ですよね。
これによって、まずはComposerのコマンド = symfony/console のコマンドとして、\Composer\Command\UpdateCommand が起動します。
そこから、やいのやいのとあって「PJのメタ情報(パッケージ定義)を読み取る」フェーズに入りますが、その時に登場するのが\Composer\Json\JsonFile::parseJson() メソッドです。
JSON構文が壊れている時に ParsingException をthrowしてくる主体も、ここにあるわけですね。
今回の変更は、まさにJsonFile::parseJson() の中身に関するものでした。
なので、その正体こそが「壊れているはずのJSONファイルを読めるようにする」秘訣だ、となる訳です。
実際の変更内容がコチラ(on GitHub)
diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php
--- a/src/Composer/Json/JsonFile.php (revision d76813b309c2ad2e992f4f72dc697017c374f1be)
+++ b/src/Composer/Json/JsonFile.php (revision 2a994c9913a282d072a25c623d512fb33fd5354b)
@@ -343,6 +343,23 @@
}
$data = json_decode($json, true);
if (null === $data && JSON_ERROR_NONE !== json_last_error()) {
+ // attempt resolving simple conflicts in lock files so that one can run `composer update --lock` and get a valid lock file
+ if ($file !== null && str_ends_with($file, '.lock') && str_contains($json, '"content-hash"')) {
+ $replaced = Preg::replace(
+ '{\r?\n<<<<<<< [^\r\n]+\r?\n\s+"content-hash": *"[0-9a-f]+", *\r?\n(?:\|{7} [^\r\n]+\r?\n\s+"content-hash": *"[0-9a-f]+", *\r?\n)?=======\r?\n\s+"content-hash": *"[0-9a-f]+", *\r?\n>>>>>>> [^\r\n]+(\r?\n)}',
+ ' "content-hash": "VCS merge conflict detected. Please run `composer update --lock`.",$1',
+ $json,
+ 1,
+ $count
+ );
+ if ($count === 1) {
+ $data = json_decode($replaced, true);
+ if ($data !== null) {
+ return $data;
+ }
+ }
+ }
+
self::validateSyntax($json, $file);
}
素朴な実装になっていますね!
-
\json_decode()をしてみて - 結果がnullで、エラーも観測されている時には
- fileの末尾が
.lockで終わっているケースにおいて -
content-hashフィールドの前後にある、コンフリクトのマーカーを、正規表現で取り除く - 取り除かれた対象が1箇所だけだったら、新しい内容で
\json_decode()を再試行して - 結果が得られたら、その結果を呼び出し元に返す
これによって、「content-hashのマージコンフリクトなら、1回スルーする」が実現されました。
それと同時に「他のパッケージ情報等のコンフリクトについては、手を加えない」ことも担保されています。
具体的にはどういうケースを解消できるの・・?については、テストのために用意されたファイルを見ると想像が付きやすいでしょう。
https://github.com/composer/composer/blob/2a994c9913a282d072a25c623d512fb33fd5354b/tests/Composer/Test/Json/Fixtures/composer-lock-merge-conflict-extended.txt の内容を抜粋すると、コレはOKです。
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
<<<<<<< HEAD
"content-hash": "e0281a92ffdb4118e47df762a8e80d8d",
=======
"content-hash": "19ff2417ff3290c12442b3d170de974e",
>>>>>>> branch-name
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}
一方で、 https://github.com/composer/composer/blob/2a994c9913a282d072a25c623d512fb33fd5354b/tests/Composer/Test/Json/Fixtures/composer-lock-merge-conflict-complex.txt はNGです。
content-hash 以外に、パッケージ情報の部分にもマージコンフリクトが見られます。2.9.0での機能のスコープ外です。
よって、引き続き「JSONが構文的におかしいので作業ができない」状態が維持されます。
終わりに
執筆時点で、Composerの最新バージョンは 2.9.2です。
どんどん進化するComposerに置いて行かれないように、みなさんも、そして私も、適切に composer self-updateしていきましょう!
-
JSON(だったもの)の中に、Gitコンフリクトを示すマーカーが入っちゃっているからですね。
<<<<<<・=======・>>>>>>>。 ↩