PHPをもっと良く書くためのLinterを作った

久々にPHPを書きたいなと思い、まず、頭の中の古い常識をクリアするために一通りphp.netを眺めていました。僕のPHPの知識は5.6辺りで止まっているのですが、ヌル合体演算子とか型宣言とか結構便利な感じになってますね。

ヌル合体演算子というのは、簡単に説明すると

<?php
// hogehoge

$val = isset($array['key']) ? $array['key'] : null;

// fugafuga

みたいなおなじみのコードを

<?php
// hogehoge

$val = $array['key'] ?? null;

// fugafuga

みたいに書けるというやつです。便利。

つまり、PHP5.6とかの常識の上で、このように書かれているコードがあれば、レビューで「ヌル合体演算子使うと楽だよー」などと指摘できるのですが、こーいうのはLinterの仕事じゃない??でもPHPにはRuboCop*1みたいな、こーいう便利なことを教えてくれるLinterが無くない??となったので、夏休みを使って作りました。

なにこれ

上のようなもっと上手く書けそうなコードのパターンを検出して、それを教えてくれるCLIです。composerで簡単に入ります。

$ composer require wata727/pahout

まぁ簡単に、というのはちょっと嘘で、php-astのextensionが必要とか、PHP7.1以上じゃないと動かないとかあるので、その辺の試行錯誤をする必要があるならば、Dockerを使うのが手っ取り早いと思います。Dockerほんと便利。

$ docker run --rm -t -v $(pwd):/workdir wata727/pahout

PHPUnitとかと同じように、vendor/binにコマンドが入るので、vendor/bin/pahoutで雑に試すことはできると思います。
例えば、PHP7.1から複数の例外をキャッチできるようになったというのも、こいつで検出できます。

<?php
try {
    hoge();
} catch (A $exn) {
    echo "catch!";
    fuga();
} catch (B $exn) {
    echo "catch!";
} catch (C $exn) { // PHP7.1からは catch (B | C $exn) でまとめられる
    echo "catch!";
}

こんな感じ

$ vendor/bin/pahout --php-version 7.1.8
test.php:7
    MultipleCatch: A catch block may specify multiple exceptions. [https://github.com/wata727/pahout/blob/master/docs/MultipleCatch.md]

1 files checked, 1 hints detected.

便利!(たぶん)

作ってて思ったこと

Phanが便利

昨年のPHPカンファレンス辺りで取り上げられて、ちょっと話題になってたLinterで、せっかくなので開発フローの中に入れました。
ちゃんと型のチェックをするためにはPHPDocをしっかり書かないといけないので、既存のプロジェクトに突然入れて、すごく生産性が上がるというタイプのものでは無いんですが、最初から入れる前提で書いていって、CIとかで通すようにしておくと便利かなと思いました。

何が便利かっていうと、単純にテスト書かなくても、ある程度実行時エラーになりそうなコードが検出できるので(もちろん、だからといってテストを書かなくていいわけでは無いが…)単純に安心感が増します。新しくPHPで何か書くならばとりあえず入れておくと良いんじゃないかと思いました。

あと、最近できたLinterということもあって、PHP7以降を前提としたコードで、よくできてるなという印象があります。今回作ったPahoutも作る上でPhanの実装をかなり参考にしています。

PHPSpecが難しかった

普段RubyRSpecを使っているので、似たような感じで書けるならこれ使ってみようかなぁと思って、ちょっと試していました。まぁ結果PHPUnitにしたんですが…
PHPSpecは単純にモデルみたいなものをテストする上では便利そうなんですけど、ちょっとIntegrationTestみたいなものを書いたり、symfony/consoleのテストヘルパみたいなものを使おうとすると、いまいち上手く書けなかったので挫折しました。界隈だとPHPUnitやPHPSpec、IntegrationTestみたいなものにはBehatを使ったりするそうですが、それぞれ実行するのクソめんどい…と思ってPHPUnitだけで書きました。結構みんな普通にそれぞれ書いてるのかな…

あと、PHPSpecだとコンストラクタで別クラスのインスタンスを生成するみたいなパターンのクラスをテストするのが難しくて、これも少し困惑しました。たぶんPHP界隈だとちゃんとDI使えって話なんでしょうけど、うーん。

DI談義だと、PHPの現場でその辺の話がありましたね。

型書けるの良い

PHP7からはメソッドの引数とか、返り値に型宣言ができるようになっていますが、便利ですね。メソッドの見通しが良くなっていいなと思います。
ただ、配列の宣言がarrayでしかできないのがちょっと悲しい。[]stringみたいな書き方させてほしい。

型の話で言うと、PHP7以降だと型書くのベストプラクティスになるかなと思って、Pahoutで型書いてないメソッドを弾けるようにしようかなと考えていました。ただ、mixedみたいな宣言ができない(宣言しないことがmixedを意味することになるのかな)ので、その場合に「本当は宣言できるのに書き忘れてる」のか、「mixedなので書けない」のか、判別する方法が無かったので諦めました。

あと、型宣言書くと遅くなるみたいな話もあるらしいですね。現実問題、パフォーマンスのために型を書かないという選択肢が生まれてくるならば、ベストプラクティスにはならないのかもしれないなぁ…

展望

普段PHPを書かないので、どこまで作れるかはあんまりわかりません。RuboCop辺りにある便利そうなCopを移植すると便利かもしれないと、ぼんやり考えています。
PHPerが犯しがちで、チェックが自動化できそうなミスのネタとか教えてくれると、とてもモチベーションになります。

*1:RubyのLinter、Style/EachWithObject みたいなのが意外と便利だったので同じものが欲しいなーと思っていた