PHPの静的解析と言えば、今は「PHPStan」の方が勢いはあります。
PHPStanは比較的新しい静的解析ツールですからね。
PHPStanと比べると、PHPMDには歴史があります。
しかし、ここで言いたいのはどっちがいいとかではありません。
むしろ、二者択一ではなく、両方とも使いましょうと言う意見です。
PHPStanに関しては、以下の記事をご覧ください。
「両方とも使いましょう」と気安く言うには、理由があります。
なぜなら、とても簡単に使えるからです。
PHPStanもPHPMDもです。
どれくらい簡単に利用できるのかは、当記事で解説しています。
本記事の内容
- PHPMDとは?
- PHPMDのインストール
- PHPMDの使い方
それでは、上記に沿って解説していきます。
PHPMDとは?
Javaには、静的解析ツール「PMD」というモノが存在します。
PHPMDは、そのPMDのPHP版を目指しているとのこと。
PHPMDが行うことは、PHPのソースコードから潜在的な問題を発見することになります。
そして、その潜在的な問題とは以下。
- 可能性のあるバグ
- 最適なコード
- 複雑すぎる表現
- 未使用のパラメータ、メソッド、プロパティ
これらの問題を見つけるために、PHPMDには多様なルールが提供されています。
静的解析であるため、実際にプログラムを動かして解析する訳でありません。
コードをルールに基づいて解析して、何かしらの問題を見つけるということです。
簡単に言うと、コードレビューを受けているようなモノでしょう。
あくまで、PHPMDは解析して、その解析結果を表示するだけです。
決して、コードを自動的に修正することはありません。
PHPMDのインストール
PHPMD公式
https://phpmd.org/
2020年12月末時点の最新バージョンは、2.9.1となります。
2020年9月23日に公開されています。
最初のバージョンである0.1.0は、2009年12月20日に公開です。
PHPMDが結構高い頻度で更新されていることを確認できます。
そして、まだまだ現役でメンテナンスもされています。
ソフトウェア要件
ポイントとしては、PHPMDはコマンドラインのPHPで動くモノだというところです。
コマンドラインのPHPは、php-cliとも表現されます。
以下のコマンドで表示されるPHPです。
$ php -v PHP 7.2.24-0ubuntu0.18.04.7 (cli) (built: Oct 7 2020 15:24:25) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies with Zend OPcache v7.2.24-0ubuntu0.18.04.7, Copyright (c) 1999-2018, by Zend Technologies
インストール
複数のインストール方法があります。
- Pharとしてインストール
- Composer経由でのインストール
- githubリポジトリからインストール
Pharとしてインストール
Composerがインストールされていないなら、「Pharとしてインストール」をおススメします。
でも、これを機会にComposerをインストールしておくのもアリですけどね。
Pharのインストールは、簡単です。
今回は、phpmd.pharをダウンロードするだけです。
最新の安定版をダウンロード可能。
$ wget -c https://phpmd.org/static/latest/phpmd.phar
Composer経由でのインストール
Composerがインストール済みなら、この方法でインストールしましょう。
今回の検証では、Composer経由でのインストールを実施しています。
以下のコマンドを実行するだけです。
$ composer require --dev "phpmd/phpmd=@stable"
インストールが完了したら、次のコマンドで確認しましょう。
$ ./vendor/bin/phpmd --version PHPMD 2.9.1
最新バージョン2.9.1がインストールされていますね。
PHPMDのインストールは、問題なく完了です。
githubリポジトリからインストール
公式サイトでは、gitコマンドを使ったインストール方法が記載されています。
正直、おススメしません。
そもそも、PHPMDの開発に参加するプログラマーに向けたモノです。
そのため、普通はgithubでのインストールは不要でしょう。
PHPMDの使い方
PHPMDの使い方は、以下。
ヘルプに結構しっかりと書いてあります。
$ ./vendor/bin/phpmd --help Mandatory arguments: 1) A php source code filename or directory. Can be a comma-separated string 2) A report format 3) A ruleset filename or a comma-separated string of rulesetfilenames Example: phpmd /path/to/source format ruleset Available formats: ansi, html, json, text, xml. Available rulesets: cleancode, codesize, controversial, design, naming, unusedcode. Optional arguments that may be put after the mandatory arguments: --minimumpriority: rule priority threshold; rules with lower priority than this will not be used --reportfile: send report output to a file; default to STDOUT --suffixes: comma-separated string of valid source code filename extensions, e.g. php,phtml --exclude: comma-separated string of patterns that are used to ignore directories. Use asterisks to exclude by pattern. For example *src/foo/*.php or *src/foo/* --strict: also report those nodes with a @SuppressWarnings annotation --ignore-violations-on-exit: will exit with a zero code, even if any violations are found
使用例は、「phpmd /path/to/source format ruleset」となります。
以下の3つの引数は必須です。
- /path/to/source
- format
- ruleset
それぞれを説明していきます。
/path/to/source
ソースコードのファイル名またはディレクトリ名を指定します。
カンマ区切りの文字列で複数指定も可能です。
format
解析結果の出力フォーマットを指定します。
次のフォーマットの中から選択します。
- ansi
- html
- json
- text
- xml
それぞれの説明は以下。
普通に「phpmd /path/to/source format ruleset」を実行すると、そのフォーマットのファイルが作成されるわけではありません。
コマンドライン上に結果が表示されます。
ansi
コマンドライン用のカラフルで整形されたテキストによる表示。
FILE: Something.class.php ------------------------------------------- 5 | VIOLATION | Avoid unused private fields such as '$FOO'. 6 | VIOLATION | Avoid unused private fields such as '$i'.
「VIOLATION」部分が赤文字にカラーリングされています。
html
htmlタグで表示。
<html><head> <script> function toggle(id) { var item = document.getElementById(id); item.classList.toggle('hidden'); } </script> <style> @media (min-width: 1366px) { body { max-width: 80%; margin: auto; } } body { font-family: sans-serif; } a { color: #2f838a; } a:hover { color: #333; } em { font-weight: bold; font-style: italic; } h1 { padding: 0.5ex 0.2ex; border-bottom: 2px solid #333; } table { width: 100%; border-spacing: 0; } table tr > th { text-align: left; } table caption { font-weight: bold; padding: 1ex 0.5ex; text-align: left; font-size: 120%; border-bottom: 2px solid #333; } tbody tr:nth-child(odd) { background: rgba(0, 0, 0, 0.08); } tbody tr:hover { background: #ffee99; } thead th { border-bottom: 1px solid #aaa; } table td, table th { padding: 0.5ex; } /* Table 'count' and 'percentage' column */ .t-cnt, .t-pct { width: 5em; } .t-pct { opacity: 0.8; font-style: italic; font-size: 80%; } /* Table bar chart */ .t-bar { height: 0.5ex; margin-top: 0.5ex; background-color: #2f838a; /* rgba(47, 131, 138, 0.2); */ } section, table { margin-bottom: 2em; } #details-link.hidden { display: none; } #details-wrapper.hidden { display: none; } ul.code { margin: 0; padding: 0; } ul.code.hidden { display: none; } ul.code li { display: flex; line-height: 1.4em; font-family: monospace; white-space: nowrap; } ul.code li:nth-child(odd) { background-color: rgba(47, 131, 138, 0.1) } /* Excerpt: Line number */ ul.code .no { width: 5%; min-width: 5em; text-align: right; border-right: 1px solid rgba(47, 131, 138, 0.6); padding-right: 1ex; box-sizing: border-box; } /* Excerpt: Code */ ul.code .cd { padding-left: 1ex; white-space: pre-wrap; box-sizing: border-box; word-wrap: break-word; overflow: hidden; } .hlt { background: #ffee99 !important } .prio { color: #333; float: right; } .indx { padding: 0.5ex 1ex; background-color: #000; color: #fff; text-decoration: none; } .indx:hover { background-color: #2f838a; color: #fff; } /* Problem container */ .prb h3 { padding: 1ex 0.5ex; border-bottom: 2px solid #000; font-size: 95%; margin: 0; } .info-lnk { font-style: italic !important; font-weight: normal !important; text-decoration: none; } .info-lnk.blck { padding: 0.5ex 1ex; background-color: rgba(47, 131, 138, 0.2); } .path-basename { font-weight: bold; } .hlt-info { display: inline-block; padding: 2px 4px; font-style: italic; } .hlt-info.quoted { background-color: #92de71; } .hlt-info.variable { background-color: #a3d2ff; } .hlt-info.method { background-color: #f7c0ff; } .sub-info { padding: 1ex 0.5ex; } /* Handle printer friendly styles */ @media print { body, th { font-size: 10pt; } .hlt-info { padding: 0; background: none; } section, table { margin-bottom: 1em; } h1, h2, h3, table caption { padding: 0.5ex 0.2ex; } .prb h3 { border-bottom: 0.5px solid #aaa; } .t-bar { display: none; } .info-lnk { display: none; } #details-wrapper { display: block !important; font-size: 90% !important; } } </style> <title>PHPMD Report</title></head><body> <header> <h1>PHPMD Report</h1> Generated at <em>2020-12-27 03:31</em> with <a href='https://phpmd.org' target='_blank'>PHP Mess Detector</a> on <em>PHP 7.2.24-0ubuntu0.18.04.7</em> on <em>ubuntu-bionic</em> </header> <h3>2 problems found</h3><h2>Summary</h2><section><table><caption>By priority</caption><thead><tr><th>Count</th><th>%</th><th>Priority</th></tr></thead><tr> <td class='t-cnt'>2</td> <td class='t-pct'>100.0 %</td> <th class='t-n'>Moderate (3)<div class="t-bar" style="width: 100%; opacity: 1.00"></div></th> </tr></table></section> <section><table><caption>By rule set</caption><thead><tr><th>Count</th><th>%</th><th>Rule set</th></tr></thead><tr> <td class='t-cnt'>2</td> <td class='t-pct'>100.0 %</td> <th class='t-n'>Unused Code Rules<div class="t-bar" style="width: 100%; opacity: 1.00"></div></th> </tr></table></section> <section><table><caption>By name</caption><thead><tr><th>Count</th><th>%</th><th>Rule name</th></tr></thead><tr> <td class='t-cnt'>2</td> <td class='t-pct'>100.0 %</td> <th class='t-n'>UnusedPrivateField<div class="t-bar" style="width: 100%; opacity: 1.00"></div></th> </tr></table></section> <h2 style='page-break-before: always'>Details</h2> <a id='details-link' class='info-lnk blck' href='#' onclick='toggle("details-link"); toggle("details-wrapper"); return false;' > Show details ▼ </a><div id='details-wrapper' class='hidden'> <section class='prb' id='p-0'> <header> <h3> <a href='#p-0' class='indx'>#1</a> Avoid unused private fields such as <span class='hlt-info quoted'>'$FOO'</span>. <a class='info-lnk' href='https://phpmd.org/rules/unusedcode.html#unusedprivatefield' target='_blank'>(help)</a> <span class='prio'>Moderate (3)</span> </h3> </header> <div class='sub-info'><b>File:</b> <a href='file:///vagrant_data/php/Something.class.php' target='_blank'>/vagrant_data/php/<span class='path-basename'>Something.class.php</span></a> <a class='info-lnk blck' href='#' onclick='toggle("p-0-code"); return false;'> Show code ▼ </a></div> <ul class='code hidden' id='p-0-code'><li><di</div></li></ul> </section>no'>7</div><div class='cd'> private $j = 6; // Unusedc $FOO = 2; // Unused <section class='prb' id='p-1'> <header> <h3> <a href='#p-1' class='indx'>#2</a> Avoid unused private fields such as <span class='hlt-info quoted'>'$i'</span>. <a class='info-lnk' href='https://phpmd.org/rules/unusedcode.html#unusedprivatefield' target='_blank'>(help)</a> <span class='prio'>Moderate (3)</span> </h3> </header> <div class='sub-info'><b>File:</b> <a href='file:///vagrant_data/php/Something.class.php' target='_blank'>/vagrant_data/php/<span class='path-basename'>Something.class.php</span></a> <a class='info-lnk blck' href='#' onclick='toggle("p-1-code"); return false;'> Show code </div></li></ul> </section>no'>8</div><div class='cd'> public function addOne() 5; // Unused='cd'>{ </div></body></html>
json
json形式の表示。
htmlよりは、こちらの方が利用しやすいですね。
{ "version": "@package_version@", "package": "phpmd", "timestamp": "2020-12-27T03:33:56+00:00", "files": [ { "file": "Something.class.php", "violations": [ { "beginLine": 5, "endLine": 5, "package": null, "function": null, "class": null, "method": null, "description": "Avoid unused private fields such as \u0027$FOO\u0027.", "rule": "UnusedPrivateField", "ruleSet": "Unused Code Rules", "externalInfoUrl": "https:\/\/phpmd.org\/rules\/unusedcode.html#unusedprivatefield", "priority": 3 }, { "beginLine": 6, "endLine": 6, "package": null, "function": null, "class": null, "method": null, "description": "Avoid unused private fields such as \u0027$i\u0027.", "rule": "UnusedPrivateField", "ruleSet": "Unused Code Rules", "externalInfoUrl": "https:\/\/phpmd.org\/rules\/unusedcode.html#unusedprivatefield", "priority": 3 } ] } ] }
text
ansiとほぼ同じです。
Something.class.php:5 Avoid unused private fields such as '$FOO'. Something.class.php:6 Avoid unused private fields such as '$i'.
xml
xml形式の表示です。
xmlは、もうjsonに取って代わられた感はあります。
個人的には、xml形式での出力はないでしょう。
<?xml version="1.0" encoding="UTF-8" ?><pmd version="@package_version@" timestamp="2020-12-27T03:40:31+00:00"> <file name="Something.class.php"> <violation beginline="5" endline="5" rule="UnusedPrivateField" ruleset="Unused Code Rules" externalInfoUrl="https://phpmd.org/rules/unusedcode.html#unusedprivatefield" priority="3"> Avoid unused private fields such as '$FOO'. </violation> <violation beginline="6" endline="6" rule="UnusedPrivateField" ruleset="Unused Code Rules" externalInfoUrl="https://phpmd.org/rules/unusedcode.html#unusedprivatefield" priority="3"> Avoid unused private fields such as '$i'. </violation> </file></pmd>
ruleset
解析する際に用いるルールです。
そのルールを指定します。
カンマ区切りの文字列で複数指定も可能です。
指定できるのは、ルールのファイル名です。
そのルールが記載されたファイルは、デフォルトで以下の分だけ用意されています。
- cleancode
- codesize
- controversial
- design
- naming
- unusedcode
それぞれの意味に関しては、公式サイトで詳しく解説されています。
https://phpmd.org/rules/index.html
また、これらのファイルは以下の場所に存在しています。
$ ls ./vendor/phpmd/phpmd/src/main/resources/rulesets/ cleancode.xml codesize.xml controversial.xml design.xml naming.xml unusedcode.xml
あと、このルールファイルは自分で作成可能です。
自作したルールを適用する場合には、ファイルパスを指定します。
phpmd /path/to/source text codesize,controversial,/my/rules.xml
公式サイトでは、ルールファイルの作成方法も解説されています。
「ルールを自分で作ってみるか」と思う方は、公式サイトをご覧ください。