PHPコードの静的解析ツールたち

複数人で開発していると、コードの書き方(インデントやブレスでの改行など)について意見が分かれることがあるかと思います。
また、PHP7.0以降で使えるタイプヒントは、宣言した型が不一致だとエラーになってくれますが、実行するまでエラーになるかわかりません。

安心安定の開発にはこれらが障害になりますので、静的解析によって排除しましょう。
今回はそのためのツールと簡単な使い方を書きます。

PHP_CodeSniffer

github.com

決められたコーディング標準を守っているかをチェックするツールです。
ここではPSR-12への準拠をチェックする例を示します。

  1. 導入
  2. ルールファイル(phpcs.xml)作成
  3. 実行
# Composerで導入
$ composer require --dev squizlabs/php_codesniffer
<!-- composer.jsonと同じ場所にphpcs.xmlを作成 -->

<?xml version="1.0"?>
<!-- ルールセット名 -->
<ruleset name="Custom_PSR12">
  <!-- 説明 -->
  <description>Custom ruleset Based on PSR12</description>
  <!-- 参照するルール( `PSR12` ) -->
  <rule ref="PSR12" />

  <!-- 拡張子を指定する(.php) -->
  <arg name="extensions" value="php" />
  <!-- 結果出力に色を付ける -->
  <arg name="colors" />
  <!-- 進捗を表示する(-p) -->
  <!-- エラー情報に、違反したルールを表示する(-s) -->
  <arg value="ps" />

  <!-- チェックから除外するディレクトリ(Laravelの場合) -->
  <exclude-pattern>/bootstrap/</exclude-pattern>
  <exclude-pattern>/config/</exclude-pattern>
  <exclude-pattern>/database/</exclude-pattern>
  <exclude-pattern>/node_modules/</exclude-pattern>
  <exclude-pattern>/public/</exclude-pattern>
  <exclude-pattern>/resources/</exclude-pattern>
  <exclude-pattern>/routes/</exclude-pattern>
  <exclude-pattern>/storage/</exclude-pattern>
  <exclude-pattern>/vendor/</exclude-pattern>
</ruleset>
# 実行(エラーが1件出ている例)
$ ./vendor/bin/phpcs --standard=phpcs.xml ./
E...........................................................  60 / 437 (14%)
............................................................ 120 / 437 (27%)
............................................................ 180 / 437 (41%)
............................................................ 240 / 437 (55%)
............................................................ 300 / 437 (69%)
............................................................ 360 / 437 (82%)
............................................................ 420 / 437 (96%)
.................                                            437 / 437 (100%)


FILE: /var/www/html/server.php
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 21 | ERROR | [x] Expected at least 1 space before "."; 0 found
    |       |     (PSR12.Operators.OperatorSpacing.NoSpaceBefore)
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 6.45 secs; Memory: 26MB

PHPMD

github.com

潜在的な問題(複雑さや依存の多さ、未使用の変数など)を検出します。
ここではカスタムルールの例を示します。

  1. 導入
  2. ルールファイル(phpmd.xml)作成
  3. 実行
# Composerで導入
$ composer require --dev phpmd/phpmd
<!-- composer.jsonと同じ場所にphpmd.xmlを作成 -->

<?xml version="1.0"?>
<ruleset name="Custom_PHPMD">
  <description>Custom ruleset PHPMD</description>
  <!-- 実行するルールを追加する -->
  <!-- ルール一覧 https://phpmd.org/rules/index.html -->
  <rule ref="rulesets/cleancode.xml">
    <!-- 除外するルール -->
    <exclude name="StaticAccess" />
  </rule>
  <rule ref="rulesets/codesize.xml" />
  <rule ref="rulesets/controversial.xml" />
  <rule ref="rulesets/design.xml" />
  <rule ref="rulesets/unusedcode.xml" />
</ruleset>
# 実行(エラーが1件出ている例)
$ ./vendor/bin/phpmd ./ text phpmd.xml
/var/www/html/app/Models/User.php:12       The class User has 13 public methods. Consider refactoring User to keep number of public methods under 10.

PHPStan

github.com

各行を静的に解析し、問題(引数の数、型の不一致など)を検出します。

  1. 導入
  2. 実行
# Composerで導入
$ composer require --dev phpstan/phpstan

# 実行(エラーが1件出ている例)
$ ./vendor/bin/phpstan analyse ./
 185/185 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ -------------------------------------------------------
  Line   Models/User.php
 ------ -------------------------------------------------------
  117    Function foo not found.
 ------ -------------------------------------------------------


 [ERROR] Found 1 error

Larastan

github.com

PHPStanのLaravel用拡張パッケージ(非公式)です。
LaravelでPHPStanを使うと、static呼び出しなどで大量のエラーが出ますので、それらをカバーしてくれます。

  1. 導入
  2. ルールファイル(phpstan.neon)作成
  3. 実行
# Composerで導入
# Larastanを使う場合、composer.jsonにはphpstan/phpstanは不要です(依存で入る)
$ composer require --dev nunomaduro/larastan
# composer.jsonと同じ場所にphpstan.neonを作成
includes:
  - ./vendor/nunomaduro/larastan/extension.neon
# 実行(エラーが1件出ている例)
$ php artisan code:analyse --level=max --paths=app,tests
 185/185 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ----------------------------------------------------------------------------------------
  Line   Models/User.php
 ------ ----------------------------------------------------------------------------------------
  105    Parameter #1 $id of method App\Model\User::has() expects int, float|int given.
 ------ ----------------------------------------------------------------------------------------


 [ERROR] Found 1 error

余談

composer.jsonのscriptsセクションには、任意のコマンドを定義できます。

"scripts": {
    "phpcs": [
        "./vendor/bin/phpcs --standard=phpcs.xml ./"
    ],
    "phpmd": [
        "./vendor/bin/phpmd app,tests text phpmd.xml"
    ],
    "phpstan": [
        "@php artisan code:analyse --level=max --paths=app,tests"
    ],
    "analyse": [
        "@phpcs",
        "@phpmd",
        "@phpstan"
    ]
}

これを使うと、長いコマンドでも以下のように簡単に使えるようになりますのでオススメです。

# PHP_CodeSnifferを実行
$ composer phpcs
> ./vendor/bin/phpcs --standard=phpcs.xml ./
............................................................  60 / 437 (14%)
............................................................ 120 / 437 (27%)
............................................................ 180 / 437 (41%)
............................................................ 240 / 437 (55%)
............................................................ 300 / 437 (69%)
............................................................ 360 / 437 (82%)
............................................................ 420 / 437 (96%)
.................                                            437 / 437 (100%)


Time: 6.41 secs; Memory: 26MB
# PHP_CodeSniffer, PHPMD, PHPStanを実行
$ composer analyse
> ./vendor/bin/phpcs --standard=phpcs.xml ./
............................................................  60 / 437 (14%)
............................................................ 120 / 437 (27%)
............................................................ 180 / 437 (41%)
............................................................ 240 / 437 (55%)
............................................................ 300 / 437 (69%)
............................................................ 360 / 437 (82%)
............................................................ 420 / 437 (96%)
.................                                            437 / 437 (100%)


Time: 6.77 secs; Memory: 26MB

> ./vendor/bin/phpmd app,tests text phpmd.xml

> @php artisan code:analyse --level=max --paths=app,tests
 435/435 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 [OK] No errors

おわりに

冒頭でも書きましたが、静的解析ツールで判断できる範囲のことはツールに任せてしまったほうが、本質的な開発に集中できます。

これらはできればCIでも実行するようにし、チームの不安や不和を少しでも解消できると良いですね。