PHPMD(PHP Mess Detector)は、PHPのソースコードを実行せずに読み、複雑すぎるメソッド・未使用の変数や引数・長すぎるクラスといった「保守コストになる書き方」を指摘する静的解析ツールです。2026年10月4日に、2.15.0(2023年12月)から約3年ぶりのリリースとなるメジャー版PHPMD 3.0.0が公開され、コマンドの書き方、設定ファイルの形式、警告の抑制方法、しきい値の判定が変わりました。この記事では3.0.0と2.15.0を同じコードで動かした結果をもとに、インストールから移行、ルールセット、警告抑制、GitHub Actionsまでを説明します。
まとめ:PHPMD 3.0の要点
- 役割:コードの構造(複雑度・サイズ・未使用コード・命名)を検査する。型はPHPStan、書式はPHP_CodeSnifferの担当で、併用が前提。
- 導入:
composer require --dev phpmd/phpmd。3.0.0はPHP 8.1以上が必要で、PHP 8.0以前の環境では2.15.0が選ばれる。 - 実行:3.0からは
phpmd analyze srcが基本形。形式とルールセットは--format・--rulesetで渡す。2.x の位置引数は非推奨の警告付きで動く。 - 移行:
phpmd migrate phpmd.xmlがプロパティ名を新しいmaximumに直してYAMLへ変換する。しきい値ちょうどの値は3.0では報告されなくなる。 - 警告抑制:
#[SuppressWarnings(ルールのクラス::class)]属性が推奨。2.xの'PHPMD.ルール名'形式の文字列は効かない。既存の違反はベースラインで凍結する。 - 注意:3.0.0の
--update-baselineは既定の並列実行だとベースラインを空にした。--threads 1を付けて実行する。
PHPMDの役割とPHPStan・PHP_CodeSnifferとの違い
PHPMDはGitHubの phpmd/phpmd リポジトリで開発されているオープンソースのツールで、ライセンスはBSD-3-Clauseです。コードメトリクスを測る PHP Depend(pdepend)を土台にしており、Javaの静的解析ツールPMDのPHP版として始まりました。PHP Dependが測った循環的複雑度やメソッドの行数を、ルールごとのしきい値と比べて違反を出す仕組みです。
PHPの静的解析ツールは見ている対象が違うため、1本に絞るより組み合わせて使います。
| ツール | 検査対象 | 代表的な指摘 |
|---|---|---|
| PHPMD | 構造・複雑さ | 複雑度超過、未使用の引数、長すぎるメソッド |
| PHPStan / Psalm | 型・呼び出し | 型の不一致、存在しないメソッド、null参照 |
| PHP_CodeSniffer / Pint | 書式・規約 | PSR-12違反、インデント、空白 |
型の検査を足すならPHPStanのレベル0〜10の違いとベースライン運用、Rust製の高速なリンターを検討するならMagoの使い方とPHPStanとの違いが参考になります。PHPMDのルールの多くは複雑度や長さといった保守性の指標で、型エラーのようにゼロを必須にするより、しきい値をチームで決めて運用します。一方で UndefinedVariable(未定義の変数)や DuplicatedArrayKey(配列キーの重複)のように、不具合そのものにつながる書き方を見つけるルールもあり、こちらは見逃さない設定にしておきます。
PHPMDのComposer導入とPHP要件
開発時だけ使うツールなので、開発依存として入れます。
composer require --dev phpmd/phpmd
vendor/bin/phpmd --version
# PHPMD 3.0.0
Packagistの登録では、3.0.0(2026-10-04)の要件は php: ^8.1、前の2.15.0(2023-12-11)は php: >=5.3.9 です。Composerは実行中のPHP(composer.json の config.platform.php を設定していればその値)と他の依存関係の制約から版を選ぶため、PHP 8.0以前の環境では2.15.0が選ばれます。2.x系に留めたい場合は composer require --dev phpmd/phpmd:^2.15 と版を指定します。Composerを使わない場合は、GitHubのリリースに添付された phpmd.phar(署名ファイル phpmd.phar.asc 付き)を使えます。
PHP 8.4以降で2.15.0を動かすと、解析結果の前に依存ライブラリの Deprecated 通知が大量に混ざります。PHP 8.4.24では「Implicitly marking parameter as nullable is deprecated」、PHP 8.5.8ではそれに加えて「Non-canonical cast (integer) is deprecated」が出ました。結果自体は変わりませんが、CIのログが読めなくなるため、PHP 8.4以降なら3.0.0へ上げるのが素直な解決です。
PHPMD 3.0の変更点と2.15からの移行
3.0.0のリリースノートとUPGRADING.mdに破壊的変更がまとまっています。2026年10月30日時点で公式サイト phpmd.org のドキュメントは2.15.0の構文のままなので、3.0を使うときはこの2つを正として読みます。
analyze コマンドへの構文変更
2.xでは「対象・出力形式・ルールセット」を順番に並べる位置引数でしたが、3.0では対象パスだけが引数で、形式とルールセットはオプションになりました。
# 2.x
phpmd src text codesize,unusedcode
# 3.0
phpmd analyze src --ruleset codesize --ruleset unusedcode
2.xの書き方も3.0.0で動きますが、実行すると Deprecated: positional arguments are deprecated, use "phpmd analyze <file> --format text --ruleset <ruleset>" instead. と表示されます。一方、次のオプションは削除されたため、指定するとエラー(終了コード1)で止まります。
| 2.x | 3.0 |
|---|---|
| –reportfile | –reportfile-text、–reportfile-xml など形式ごと |
| –minimumpriority | –minimum-priority |
| –maximumpriority | –maximum-priority |
| –ignore | –exclude |
| –extensions | –suffixes |
CIのスクリプトで --reportfile を使っている場合、PHPMDのバージョンを上げた瞬間にジョブが落ちます。composer.lock で版を固定し、スクリプトを直してから上げてください。
しきい値の判定方法と maximum への名称統一
上限を見るルールのプロパティ名が maximum に統一され、判定も「設定値を超えたら報告」にそろいました。CyclomaticComplexity は reportLevel、TooManyMethods は maxmethods、ExcessiveMethodLength は minimum だったものが、すべて maximum になります。古い名前も別名として読み込まれます。
影響が大きいのは「しきい値ちょうど」の扱いです。if文を9個並べて循環的複雑度を10にしたメソッドを既定の codesize で解析すると、2.15.0は「Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10.」と報告し、3.0.0は報告しませんでした。3.0では設定値が「許容する最大値」になったためです。複雑度の数え方そのものはサイクロマティック複雑度の計算方法と目安10の根拠で説明しています。
phpmd migrate による設定ファイルの移行
2.xの phpmd.xml は phpmd migrate で3.0の形式に変換できます。--dry-run を付けると書き込まずに結果だけ表示します。循環的複雑度の上限を8にしていた設定で試した出力は次のとおりです。
vendor/bin/phpmd migrate phpmd.xml --dry-run
rules:
- ref: rulesets/unusedcode.xml
- ref: rulesets/codesize.xml/CyclomaticComplexity
properties:
maximum: 8
* Renamed property reportLevel of rule CyclomaticComplexity to maximum.
2.xと同じ範囲を報告し続けたいなら --preserve-behavior を付けます。同じ設定で「Lowered maximum of rule CyclomaticComplexity from 8 to 7」と表示され、値が1つ下がりました。ただし調整されるのは設定ファイルに値を書いたルールだけで、既定値のまま使っているルールは3.0の新しい既定値で判定されます。既定の循環的複雑度10ちょうども報告させたいなら、そのルールを参照して maximum: 9 を明示します。元のXMLは残るので、変換後のYAMLを確認してから削除します。
phpmd analyze の使い方と主なオプション
3.0.0で --ruleset を省くと、後述の phpmd.yml などの設定ファイルがあればその設定を、無ければ6つのルールセットすべてを使います。--format を省くと text で出力します。
vendor/bin/phpmd analyze src
vendor/bin/phpmd analyze src app --ruleset cleancode --ruleset codesize
vendor/bin/phpmd analyze src --format json --reportfile-xml phpmd-report.xml
出力形式は ansi・baseline・checkstyle・github・githubcheckruns・gitlab・html・json・sarif・text・xml の11種類です。画面には --format の形式、ファイルには --reportfile-xml などで別の形式を同時に書き出せます。
| オプション | 役割 |
|---|---|
| –exclude ‘*/tests/*’ | 除外するパス(繰り返し指定可) |
| –minimum-priority 2 | 優先度1〜2のルールだけ使う |
| –strict | 抑制した箇所も報告する |
| –ignore-violations-on-exit | 違反があっても終了コード0 |
| –threads 4 | 並列で解析する(既定はCPUコア数) |
| –cache | 変更の無いファイルの結果を再利用する |
| –no-progress | 進捗バーを出さない |
3.0.0から進捗バーが標準エラー出力に出るので、ログを残すCIでは --no-progress を付けておくと出力が読みやすくなります。
終了コードとCIでの扱い
終了コードは、違反なしが0、違反ありが2、構文エラーなどで解析できないファイルがあると3です。存在しないオプションを渡したときは1でした。CIで「違反は記録するがジョブは落とさない」段階から始めるなら --ignore-violations-on-exit を付け、運用が固まったら外して品質ゲートにします。
–threads による並列解析と計測結果
3.0.0で追加された --threads の既定値はCPUのコア数です。並列化はプロセスを分岐させる pcntl_fork を使うため、pcntl拡張が有効でComposerから入れたPHPMDに限られ、PHAR版やpcntlの無いWindowsでは1プロセスで動きます。
並列にすれば速くなるとは限りません。symfony/console と pdepend/pdepend の835ファイルを codesize で解析し、PHP Dependのキャッシュ(~/.pdepend)を毎回消して2回ずつ測ったところ、論理16コアのmacOS・PHP 8.5.8で --threads 1 が8.27秒と11.10秒、--threads 4 が14.84秒と16.03秒、--threads 16 が14.84秒と14.04秒でした。この規模ではプロセスを分ける手間が上回り、1プロセスのほうが速い結果です。CIでは --threads 1 と既定値の両方で実行時間を測り、速いほうを設定に書いてください。
6つのルールセットと3.0で増えたルール
同梱のルールセットは2.xと同じ6つで、3.0.0で有効なルールは合計50個です(XML内でコメントアウトされたルールは数えていません)。
| ルールセット | ルール数 | 代表的なルール |
|---|---|---|
| cleancode | 10 | ElseExpression、StaticAccess、MissingImport |
| codesize | 10 | CyclomaticComplexity、NPathComplexity、TooManyMethods |
| controversial | 7 | Superglobals、CamelCaseClassName |
| design | 9 | ExitExpression、EvalExpression、CouplingBetweenObjects |
| naming | 9 | ShortVariable、LongVariable、LongMethodName |
| unusedcode | 5 | UnusedLocalVariable、UnusedFormalParameter、UnusedSuppression |
3.0.0で増えたのは、条件がリテラルだけのif文を指摘する IfStatementWithoutLogic(cleancode)、メソッド名が40文字を超えると指摘する LongMethodName(naming)、何も抑制していない属性を指摘する UnusedSuppression(unusedcode)の3つです。if (true) { ... } を含むコードは、2.15.0では何も出ず、3.0.0では「Avoid if statement without logic」と報告されました。
最初に入れるなら codesize と unusedcode です。codesize は複雑度や行数を数値で示し、unusedcode は未使用の変数・引数・privateメソッドを名指しするため、直す箇所と直し方がはっきりしています。controversial と naming は命名の流儀に強く依存するため、Laravelの命名規則とコーディング規約のようなチームの基準を決めてから、使うルールを選びます。
phpmd.yml のルール設定と自動検出
3.0ではXMLに加えてYAML・JSON・PHPで設定を書けるようになり、YAMLが推奨形式です。カレントディレクトリの phpmd.yml、phpmd.yaml、phpmd.json、phpmd.xml、phpmd.php(先頭に . を付けた名前や .dist 付きも可)が自動で読まれるため、--ruleset を毎回渡す必要がありません。
ひな形は phpmd init の対話形式で作れます。-n を付けて質問をすべて既定で答えると、次のファイルができました(name にはディレクトリ名が入ります)。
# PHPMD configuration, see https://phpmd.org/documentation/creating-a-ruleset.html
name: myapp
paths:
- src
cache: true
rules:
- ref: rulesets/cleancode.xml
- ref: rulesets/codesize.xml
- ref: rulesets/controversial.xml
- ref: rulesets/design.xml
- ref: rulesets/naming.xml
- ref: rulesets/unusedcode.xml
paths を書いておくと、引数なしの vendor/bin/phpmd analyze で src が解析されます。しきい値を変えるときは、ルールを個別に参照して properties で上書きします。
name: project
paths:
- src
exclude-pattern:
- '*/tests/*'
rules:
- ref: rulesets/unusedcode.xml
- ref: rulesets/codesize.xml/CyclomaticComplexity
properties:
maximum: 8
しきい値が厳しすぎると感じたときは、ルールを外す前に maximum を調整します。ルールごと外すと、本当に分割すべき巨大なメソッドまで見逃します。なお、PHP 8.3.30環境で同梱のXMLルールセットを読み込むと「trim(): Argument #1 ($string) must be of type string, SimpleXMLElement given」で止まるという報告(Issue #1364)があります。修正のPR #1365は2026年10月20日に3.xブランチへマージされましたが、2026年10月30日時点のリリースは3.0.0のみで、修正を含む版はまだ出ていません。PHP 8.4.24と8.5.8の手元環境では再現しませんでした。
特定の警告だけを無視する方法とベースライン運用
既存のコードにPHPMDを入れると、初回は大量の違反が出ます。ルールを外して黙らせるより、抑制する範囲を絞るほうが検査の意味が残ります。3.0では抑制の書き方とベースラインの動きが変わったので、2.xの記事のとおりに書くと効かない箇所があります。
#[SuppressWarnings] 属性による警告抑制
3.0の推奨はPHP 8の属性です。引数にはルールのクラスを ::class で渡します。
use PHPMD\Attribute\SuppressWarnings;
use PHPMD\Rule\UnusedFormalParameter;
class Handler
{
#[SuppressWarnings(UnusedFormalParameter::class)]
public function handle($request, $unused)
{
return $request;
}
}
同じメソッドで書き方だけを変えて試した結果は次のとおりです。
| 書き方 | 抑制 | UnusedSuppression |
|---|---|---|
| #[SuppressWarnings(UnusedFormalParameter::class)] | される | 出ない |
| #[SuppressWarnings(‘PHPMD.UnusedFormalParameter’)] | されない | 出る |
| #[SuppressWarnings(‘UnusedFormalParameter’)] | されない | 出る |
| #[SuppressWarnings](引数なし) | 全ルール | 出ない |
| @SuppressWarnings(PHPMD.UnusedFormalParameter) | される | 対象外 |
2.xのドキュメントコメントの書き方を属性に移すとき、'PHPMD.ルール名' の文字列をそのまま渡すと抑制されず、元の違反に加えて UnusedSuppression も出ます。文字列でも 'PHPMD\Rule\UnusedFormalParameter' のように完全修飾クラス名を書けば抑制されました。ルールのクラスは PHPMD\Rule\UnusedFormalParameter のように名前空間がルールセットごとに違うため(ElseExpression は PHPMD\Rule\CleanCode\ElseExpression)、IDEの補完で use を入れると確実です。属性のクラスはリフレクションの newInstance() で実体化されるまで読み込まれないため、本番環境を composer install --no-dev にしてPHPMDが無くても、属性を実体化するライブラリを使っていない限りエラーにはなりません。
ドキュメントコメントの @SuppressWarnings は非推奨になりましたが、3.0.0でも抑制は効きます。ただし UnusedSuppression の対象外なので、コードを直した後に不要な抑制が残っても気づけません。抑制した箇所をまとめて確認したいときは --strict を付けると、属性とコメントの両方を無視して全件を報告します。
ベースラインの生成と照合単位
レガシーコードでは、今ある違反をベースラインに記録し、新しく増えた違反だけを報告させます。
vendor/bin/phpmd analyze src --generate-baseline
できる phpmd.baseline.xml の中身は次の形です。
<phpmd-baseline>
<violation rule="PHPMD\Rule\UnusedFormalParameter" file="src/Order.php"/>
<violation rule="PHPMD\Rule\UnusedLocalVariable" file="src/Order.php"/>
</phpmd-baseline>
記録は行番号を持たず、ルールとファイルの組み合わせ(メソッド名が付く違反はメソッドも)で照合します。そのため、ベースラインに載ったファイルへ同じルールの違反を新しく足しても報告されません。記録済みのファイルに、未使用の引数とローカル変数を持つメソッドを追加した検証では、2.15.0・3.0.0とも終了コード0のままでした。ベースラインは「ファイル単位で負債を棚上げする仕組み」と捉え、巨大なファイルほど早めに分割して記録から外していくのが安全です。
–update-baseline の仕様変更と並列実行時の不具合
直した違反をベースラインから消す --update-baseline は、3.0.0で動きが変わりました。ベースラインに無い違反は通常どおり報告されて終了コード2になり、ベースラインには追加されません(2.xでは黙って捨てられていました)。
注意が必要なのは並列実行との組み合わせです。pcntl拡張が有効なComposer版で、違反が1件も減っていない状態のまま3.0.0の既定(CPUコア数で並列)で --update-baseline を実行すると、phpmd.baseline.xml の中身が空になりました。--baseline-file や --ruleset を明示しても同じで、--threads 1 を付けたときだけ記録が残りました。空になったベースラインのまま次の解析を回すと、凍結していた違反がすべて報告されます。
vendor/bin/phpmd analyze src --update-baseline --threads 1
git diff phpmd.baseline.xml
更新後は必ず差分を見て、消えた行が本当に直した違反か確かめてからコミットしてください。
GitHub ActionsでのPHPMD実行と警告表示
プルリクエストごとに実行すると、レビュー前に構造の問題が見えます。版を composer.lock で固定できるよう、Composerで入れたPHPMDを呼ぶ構成にします。
name: PHPMD
on: [push, pull_request]
jobs:
phpmd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- run: composer install --no-interaction --no-progress
- run: vendor/bin/phpmd analyze src --format github --no-progress
--format github は ::warning file=(ファイルの絶対パス),line=8::Avoid unused parameters such as '$flag'. の形式で出力し、プルリクエストの差分の該当行に注釈が付きます。GitLabなら --format gitlab をCode Qualityのレポートに渡します。静的解析をCI全体の品質ゲートとして組み立てる作業は、DevOps・CI/CD導入支援でも扱っています。
よくある質問
PHPMDとPHPStanはどちらを使うべきですか?
役割が違うので両方使います。PHPStanは型の不一致や存在しないメソッドの呼び出しを、PHPMDは複雑すぎるメソッドや未使用のコードを見つけます。同じ未使用変数でも、PHPStanはレベル10でも報告しない場合があり、PHPMDの unusedcode が拾います。
PHPMDは無料で使えますか?
無料です。ライセンスはBSD-3-Clauseで、商用プロジェクトでも費用はかかりません。
2.15のコマンドは3.0でもそのまま動きますか?
phpmd src text codesize のような位置引数は、非推奨の警告を表示したうえで動きます。--reportfile や --minimumpriority は削除されたため、エラーで止まります。
2.xの phpmd.xml は3.0でも使えますか?
使えます。カレントディレクトリにあれば自動で読み込まれ、reportLevel などの古いプロパティ名も受け付けます。ただし、しきい値ちょうどの値は報告されなくなります。phpmd migrate --preserve-behavior は設定ファイルに書いた値だけを1つ下げるので、既定値のまま使っているルールも2.xと同じ範囲にしたい場合は、そのルールの maximum を明示します。
PHPMDが動かない・エラーが出るときは?
終了コード3なら、構文エラーなどで解析できないファイルがあります。3.0.0はPHP 8.1未満では入らないため、古いPHPでは2.15.0を使います。PHP 8.3で「SimpleXMLElement given」を含む TypeError が出る場合は Issue #1364 と同じ症状なので、修正を含む次の版を待つか2.15.0に戻します。メモリ不足なら php -d memory_limit=512M vendor/bin/phpmd analyze src のように上限を上げます。