依存関係の地獄から抜け出せ:`composer-unused`で実現する、PHPプロジェクトの超軽量化とセキュリティ要塞化
テックリードの役割とは、ただ動くコードを書くことではない。「将来の技術負債を未然に焼き払い、開発チームがマッハで機能を出荷できる環境を維持すること」だ。
長年運用されているPHPモノリスや、幾度ものピボットを繰り返したLaravel/Symfonyアプリケーションの `composer.json` を開いてみてほしい。
「とりあえず動くから」と追加され、誰もコードのどこからも呼ばれていない「ゴースト・パッケージ」がいくつ眠っているだろうか?
未使用のパッケージの放置は、単なる容量の無駄遣いではない。
- CI/CDパイプラインの肥大化: 不要なパッケージのダウンロードとautoload生成に毎ビルド貴重な時間が奪われる。
- サプライチェーン攻撃リスクの増大: 使っていないライブラリの依存先(Transitive Dependencies)に脆弱性が発見された場合でも、存在に気づきすらしず、セキュリティスキャンで無駄にアラート対応に追われる。
- 認知負荷の増大: 「このリポジトリのドメインロジックに必要な依存関係は何なのか」というメンタルモデルを汚染する。
今回は、このデッドコードの温床を完全自動で駆逐し、プロダクトの健全性を極限まで高める `icanhazstring/composer-unused` の実戦投入フローを、プロのアーキテクチャ視点から徹底解説する。
—
1. なぜ `composer-unused` なのか?(内部動作のメカニズム)
世の中には `composer outdated` や安全性を確認する `composer audit` があるが、これらは「バージョン」や「既知の脆弱性」を見るものであり、「ソースコード内で実際にクラスや関数が使われているか」までは判定してくれない。
`composer-unused` は、以下の極めてロジカルなアプローチで未使用パッケージを検知する。
1. AST(抽象構文木)解析: プロジェクト内のPHPファイルをNikic/PHP-Parserを用いて解析し、`use`文や完全修飾名、文字列としてのクラス名(DIコンテナの設定など)を抽出する。
2. Composer Autoloadの逆引き: `composer.json` の `autoload` / `autoload-dev` で定義された名前空間と、実際にコード内で使用されている名前空間のシンボルを突き合わせる。
3. パッケージマッピング: どのクラスや関数が、どのVendor(パッケージ)に属しているかをComposerのメタデータから逆引きし、一度も参照されなかったパッケージを「Unused」としてリストアップする。
つまり、単なる文字列マッチングではなく、PHPの言語仕様とComposerの仕様に則った正確な静的解析を行っているため、誤検知が非常に少ないのが特徴だ。
—
2. 導入とプロダクション・グレードの設定
まずはプロジェクトの開発依存(`–dev`)としてインストールする。プロダクションのランタイムに含める必要は一切ない。
composer require –dev icanhazstring/composer-unused
ベストプラクティス:`composer-unused.php` の極限設定
プロジェクトのルートディレクトリに `composer-unused.php` を配置する。JSONではなくPHPで設定を書くことで、動的なパスの解決や複雑な条件分岐にも耐えられる設計になっている。
以下の設定は、LaravelやSymfonyなどのモダンフレームワークで発生しがちな「フレームワーク特有の暗黙的な依存(サービスプロバイダや設定ファイル内でしか使われないパッケージ)」を適切に除外するプロ仕様の構成だ。
addNamedPath(NamedPackage::fromBase(‘app’, Path::canonicalize(__DIR__ . ‘/app’)))
->addNamedPath(NamedPackage::fromBase(‘database’, Path::canonicalize(__DIR__ . ‘/database’)))
->addNamedPath(NamedPackage::fromBase(‘config’, Path::canonicalize(__DIR__ . ‘/config’)))
// 【重要】コード内では直接呼ばれないが、フレームワークやDIコンテナで必須のパッケージをホワイトリスト化
->addExcludes([
// LaravelのデータベースマイグレーションやSeederで動的に呼ばれるパッケージ
‘fakerphp/faker’,
// AWS SDKなど、インフラストラクチャ層の抽象化で設定ファイル経由のみでインスタンス化されるもの
‘aws/aws-sdk-php’,
// PHPUnit自体は直接コードで呼ばないが、Dev環境で必須のため除外
‘phpunit/phpunit’,
]);
};
—
3. チーム開発を加速する実践フローとCI統合
このツールを「たまに気まぐれで実行するツール」にしてはならない。CI/CDパイプラインに組み込み、「未使用パッケージが含まれるプルリクエストはマージさせない」という強固なガバナンスを敷く。
実行コマンドと出力の見方
ローカルで実行する場合は以下のコマンドを叩く。
vendor/bin/composer-unused
正常に依存関係がクリーンな状態であれば、以下のような美しい緑の出力と共に終了する。
Collected 45 packages.
Found 82 usages.
All dependencies are used!
もし、不要なパッケージ(例:古いログライブラリ `monolog/monolog` やユーティリティなど)が残っている場合は、以下のように赤色で検知される。
Unused packages:
- monolog/monolog (Loaded in composer.json, but no usages found)
これを検知したら、迷わず以下のコマンドでパージする。
composer remove monolog/monolog
GitHub Actionsワークフローへの組み込み
チーム全員が意識せずとも、自動的に依存関係の健康診断が行われるようにする `.github/workflows/composer-unused.yml` の実例だ。
name: “Composer Unused Check”
on:
pull_request:
branches:
- main
- develop
paths:
- ‘composer.json’
- ‘composer.lock’
- ‘composer-unused.php’
jobs:
unused:
name: “Check Unused Dependencies”
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ‘8.3’
tools: composer:v2
- name: Get Composer Cache Directory
id: composer-cache
run: echo “dir=$(composer config cache-files-dir)” >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${