Composerの依存関係地獄を救う:`why`と`why-not`コマンドを使ったパッケージ競合解消術
開発現場において、CI/CDパイプラインが突如として赤く染まる瞬間ほど絶望的なものはない。特にPHPの大規模モノリスや、数十のマイクロサービスが複雑に絡み合うレポジトリにおいて、`composer update` や `composer install` が以下のエラーを吐いて停止したとき、君はどうしているだろうか。
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires symfony/http-foundation ^6.4 -> satisfiable by symfony/http-foundation[v6.4.0].
- symfony/http-foundation v6.4.0 conflicts with symfony/symfony v5.4.0.
- Root composer.json requires symfony/symfony v5.4.0 -> satisfiable by symfony/symfony[v5.4.0].
多くのエンジニアは、このメッセージを見た瞬間にパニックに陥り、`composer.lock` を削除し、`vendor/` を吹き飛ばし、祈りながら再度コマンドを叩く。しかし、それは問題を隠蔽しているに過ぎない。真のDevOpsアーキテクトであれば、依存関係グラフの内部数学を理解し、CLIツールを駆使して一撃で矛盾の根源を断ち切るべきだ。
今回は、Composerの内部アーキテクチャに踏み込み、`composer why` と `composer why-not` を駆使して依存関係地獄を論理的かつ機械的に解体する、最高峰のデバッグ戦略を伝授する。
—
1. 依存関係解決エンジン(SATsolver)の内部挙動を理解する
なぜComposerは競合を起こすのか?それを知るには、裏で動いているアルゴリズムを知る必要がある。
Composerは、バージョン制約を満たすパッケージの組み合わせを見つけるために、Boolean satisfiability problem (SATソルバー) のアルゴリズムを使用している。有向グラフ(Directed Acyclic Graph: DAG)として表現されたパッケージ群の中から、すべての制約(Constraints)を同時に満たす解(Solution)を探索する。
[Root Package]
├── requires ──> [Package A (^2.0)]
│ └── requires ──> [Package C (^1.0)]
└── requires ──> [Package B (^3.0)]
└── conflicts ──> [Package C (>=1.5)] <-- 【ここで破綻】
プロジェクトが巨大化し、推移的依存関係(Transitive Dependencies:間接的に要求される依存関係)が何百、何千を超えると、このグラフは人間が脳内だけで追跡不可能な複雑さに達する。ここで必要になるのが、Composerに内蔵された強力なクエリツール、`why` と `why-not` である。
---
2. `composer why`: 「なぜこのパッケージが存在するのか」の完全追跡
ある日、プロジェクトの `vendor/` 内に見覚えのない古いパッケージが存在し、セキュリティ脆弱性スキャナー(Roave/Security Advisories等)に検知されたとする。直感的に「俺たちはこんなもの要求していない」と思いがちだが、現実はそう甘くない。
ここで以下のコマンドを実行する。
composer why monolog/monolog
実行出力例と解読
monolog/monolog 2.9.0 requirespsr/log (^1.1 || ^2.0 || ^3.0)
symfony/monolog-bridge v6.4.0 requires monolog/monolog (^1.28 || ^2.9.1 || ^3.10)
laravel/framework v10.48.0 requires symfony/monolog-bridge (^6.2)
この出力から、何が読み取れるだろうか?
1. `laravel/framework` が `symfony/monolog-bridge` を要求している。
2. その `symfony/monolog-bridge` が `monolog/monolog` を要求している。
つまり、`monolog/monolog` を単体で削除することは不可能であり、それを排除したければ、大元の `laravel/framework` のバージョンを下げるか、ブリッジパッケージを置き換える必要があることが一瞬で判明する。
パフォーマンス最適化ハック:大規模プロジェクトにおける高速クエリ
数千のパッケージを持つモノリスレポジトリでは、`composer why` は `composer.lock` をパースするため、わずか数秒のオーバーヘッドが発生する。CI環境やエディター拡張(VSCodeのPHP Tools等)から非同期で叩く場合、Composerのグローバルキャッシュを有効活用しつつ、不要なネットワークアクセスを遮断するために以下のフラグを組み合わせよ。
リモートリポジトリへの問い合わせを行わず、ローカルのlockファイルから高速に依存経路を特定する
composer why –no-update –no-scripts vendor/package-name
—
3. `composer why-not`: 競合の未来予測と矛盾の特定(真骨頂)
依存関係地獄の本質は、「あるパッケージのバージョンを上げたいが、別の何かがそれを阻んでいる」という状況にある。ここで絶大な威力を発揮するのが `composer why-not` だ。
例えば、最新のPHP 8.3への移行に伴い、PHPUnitを `^10.0` にアップグレードしたいとする。しかし、`composer update phpunit/phpunit` を実行するとエラーになる。事前にその原因を正確に知るために、以下のコマンドを叩く。
composer why-not phpunit/phpunit 10.5.0
実行出力例と解読
my-company/legacy-core v1.2.0 requires phpunit/phpunit (^9.5)
このコマンドは、「なぜ指定したバージョン(`10.5.0`)がインストールできないのか、どのパッケージが足枷になっているのか」を逆引きで暴き出す。上記の例であれば、自社製プライベートパッケージ `my-company/legacy-core` が古い `^9.5` を固定していることが一目瞭然となる。
複数のパッケージが絡み合う複雑な競合の場合も、`why-not` はすべての障壁をリストアップする。
composer why-not doctrine/orm ^3.0
出力結果に現れたすべての「阻害要因」に対して、それぞれのアップグレードパスを検討するという、極めてロジカルなデバッグアプローチが可能になるのだ。
—
4. CI/CDパイプラインへの組み込み:依存関係の「静的解析」の自動化
プロのDevOpsエンジニアであれば、人間が手動で `why` や `why-not` を叩くフェーズすら自動化する。不適切なパッケージの持ち込みや、セキュリティ上危険なバージョンの混入をCIの段階で完全にブロックするためのGitHub Actionsワークフローの設計例を示す。
`.github/workflows/composer-dependency-check.yml`
name: Composer Dependency Governance
on:
pull_request:
paths:
- ‘composer.json’
- ‘composer.lock’
jobs:
dependency-audit:
runs-name: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup PHP Environment
uses: shivammathur/setup-php@v2
with:
php-version: ‘8.3’
tools: composer:v2
- name: Validate composer.json and composer.lock
run: composer validate –strict –no-check-all
# 構文エラーやロックファイルの不整合を厳格にチェック
- name: Check for known vulnerable packages
run: composer audit
# Composer公式のセキュリティアドバイザリーDBと照合
- name: Verify Forbidden Packages (e.g., deprecated packages)
run: |
# 廃止予定のパッケージが混入していないかを why で強制チェック
FORBIDDEN_PACKAGES=(“phpunit/phpunit:^8.0” “zendframework/”)
EXIT_CODE=0
for pkg in “${FORBIDDEN_PACKAGES[@]}”; do
echo “Checking if forbidden package $pkg is required…”
if composer why “$pkg” –no-update | grep -q “Root”; then
echo “::error::Forbidden package $pkg is directly required by root.”
EXIT_CODE=1
fi
done
exit $EXIT_CODE
このパイプラインを導入することで、開発者がレビュー依頼を出す前に、依存関係のレギュレーション違反を自動検知し、プルリクエストを差し戻すことが可能になる。
—
5. Dockerコンテナ環境におけるComposerキャッシュの極限最適化
大規模な依存関係を持つプロジェクトでは、ビルド時間の大部分がComposerの依存関係解決とダウンロードに費やされる。Docker環境において、これを最速化するためのマルチステージビルドとキャッシュ戦略を提示する。
`Dockerfile`
==========================================
Stage 1: Dependency Resolution & Vendor Build
==========================================
FROM composer:2.7 AS builder
WORKDIR /app
依存関係定義ファイルのみを先にコピー(レイヤーキャッシュのヒット率を最大化)
COPY composer.json composer.json
COPY composer.lock composer.lock
Composerの並列処理数最大化と、インタラクティブモードの無効化
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_PROCESS_TIMEOUT=600
開発用依存関係を含めずに高速インストール(本番向け)
独自のプライベートレポジトリ認証が必要な場合はここで秘密情報を安全にマウントする
RUN –mount=type=cache,target=/tmp/cache \
composer config cache-dir /tmp/cache && \
composer install \
–no-dev \
–no-interaction \
–no-scripts \
–prefer-dist \
–optimize-autoloader
==========================================
Stage 2: Runtime Production Image
==========================================
FROM php:8.3-fpm-alpine
WORKDIR /var/www/html
ビルダーイメージからvendorディレクトリのみを高速転送
COPY –from=builder /app/vendor /var/www/html/vendor
COPY . /var/www/html
権限の適切な設定
RUN chown -R www-data:www-data /var/www/html/vendor
USER www-data
CMD [“php-fpm”]
このDockerfileでは、`–mount=type=cache` を用いることで、Dockerビルド間でComposerのダウンロードキャッシュ(Zipファイル等)が永続化される。これにより、2回目以降のビルドではネットワークI/Oがほぼゼロになり、ビルド時間が劇的に短縮される。
—
6. まとめ:依存関係を「支配する」ということ
依存関係地獄は、運や勘で解決するものではない。それは純粋なグラフ理論と論理学の世界である。
- どうしても入ってしまうパッケージの正体を暴くには `composer why`
- アップグレードを阻む呪縛の元凶を特定するには `composer why-not`
- そしてそれらをCI/CDとDockerで完全に自動化・統制する。
このアプローチを身につけた瞬間から、君のプロジェクトから「なぜか動かない」というエンジニアリングの怠惰は消え去る。ツールを骨の髄まで使いこなし、美しく、堅牢なデベロップメント・ライフサイクルを構築してほしい。