見出し画像

静的解析のために、ORMの補完用PHPDoc生成ライブラリを作った

静的解析を補完するPHPDoc生成ライブラリを作った記事です。静的解析の導入記事はこちらから。

静的解析補完PHPDoc生成ライブラリ

より厳格なルールを適用するために、下記のライブラリを作りました。Yii ORM ActiveRecordのPHPファイルを解析、MySQLのデータベースと照合し、PHPDocを付与します。

名前は、laravel-ide-helperから発想をもらいました。githubの組織アイコンは、弁護士ドットコムサイトのマスコットキャラクター「ほうすけ」です。かわいいですね。

このプログラムを使用すると、下記のようなPHPDocが自動生成されます。既にPHPDocがある程度書かれていても、差分だけを付与します。

laravelのlaravel-ide-helperで自動的に付与されるPHPDocと同じイメージです。Yii1で使えそうなライブラリがなかったので、自作しました。

フレームワークに縛られない作りなので、多少手元で調整すれば、どんなフレームワークにも対応できます。

/**
 * class News
 */
class News extends CActiveRecord
/**
 * class News
 *
 * @property int $id
 * @property string|null $name
 * @property NewsTopImage|null $HeadImage
 * @property NewsTopImage[] $Image
 */
class News extends CActiveRecord

ORMのマジックメソッド経由の参照は、静的解析不可

Yii ORMでは、データベースに接続してカラム情報を取得し、マジックメソッド経由でマッピングし、オブジェクトからのプロパティ参照を実現しています。laravelのEloquentと同じですね。

しかし静的解析だと、マジックメソッドによるプロパティへのアクセスはわかりません。DBに接続しませんし、PHPファイルには、該当のプロパティが存在しないからです。

そのため、下記のようなエラーが出てしまいます。

------ ----------------------------------------------------------
 Line   models/Prefecture.php
------ ----------------------------------------------------------
 12    Access to an undefined property Prefecture::$id.
------ ----------------------------------------------------------

これを回避する方法は二つあります。PHPStanは、PHPDocを解釈してくれるので、補完用のpropertyを書く方法。下記のような、PHPStan専用の拡張を書く方法です。

PHPStan専用になるので、IDE等の他の静的解析の補完に使えず、汎用性が低いです。また、静的解析するために、DBに接続するのは全く静的ではありません。

今後、psalmや他の静的解析を導入する可能性もあり、PHPDocで対応することにしました。

対象となるORMのPHPファイルは約300

MySQLのテーブルが300くらいあるので、対象となるORM classも約300あります。手動で修正は厳しく、メタプログラミングで付与します。

対象のアプリは、Yii1を使用していますが、フレームワークに縛られると使い勝手が悪くなります。古いフレームワークため、フレームワーク移行の話も出ており、極力柔軟な作りにしたい背景がありました。

そこで、シンプルにPHPファイルを解析して、付与するようにしました。

nikic/PHP-ParserでのPHP解析

PHPファイルの解析といえば、お約束のこちら。

PHPで書かれたPHPパーサーです。PHPだけで動くので、使いやすいです。

PHP 5.2~7.4のコードを解析し抽象構文木に変換、それをヒューマンリーダブルなphpファイルに出力できます。変更前後の抽象構文木を比較することで、最小限の変更におさえられます。

今回は、class名取得、PHPDocの取得・変更、関数に定義された内容の読み取りに使用しました。

データベース接続してメタ情報からPHPDoc生成

PHP-Parserで取得したclass名からMySQLに接続します。PDOでは、getColumnMeta関数で、下記のカラム情報が取得できます。接続するデータベースの種類によって、取得できる情報が変わるので注意が必要です。

array(7) {
 ["native_type"]=>
 string(10) "VAR_STRING"
 ["pdo_type"]=>
 int(2)
 ["flags"]=>
 array(2) {
   [0]=>
   string(8) "not_null"
   [1]=>
   string(11) "primary_key"
 }
 ["table"]=>
 string(17) "Account"
 ["name"]=>
 string(2) "id"
 ["len"]=>
 int(765)
 ["precision"]=>
 int(0)
}

今回は、pdo_typeの値をみて型を判断しました。このデータは、PDOの定義気済み定数により、カラムのタイプが表現されています。

もし対象のアプリで、エミュレートモードが有効な場合は、全て型がstringになるので、pdo_typeをみる必要はありません。

$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

さらに、flagsでnot_nullが設定されていなければ、nullableです。なので、追加でnullを付与します。

 * @property int $id
 * @property string|null $name

ORMに定義されているリレーションからPHPDoc生成

Yiiの場合、リレーションの対象classを関数内に定義し、マジックメソッド経由でプロパティアクセスできます。そのため、こちらも静的解析できず、PHPDocで補完する必要があります。

(new News)->HeadImage->getUrl();

relations関数に、取得可能なオブジェクトが配列で定義されています。PHP-Parserで、定義内容を取得して、PHPDocに付与していきました。

   /**
    * @inheritDoc
    */
   public function relations()
   {
       return [
           'HeadImage' => [self::BELONGS_TO, 'NewsTopImage', 'News_id'],
           'Image' => [self::HAS_MANY, NewsTopImage::class, 'News_id'],
       ];
   }

上記の例だと、連想配列のキー名が参照時のプロパティ名、連想配列の値のインデックス0が結びつくオブジェクトの関係性でBELONGS_TO等の場合は1対1、HAS_MANYなどの場合は、1対nになります。

1対nやn対nの場合は、オブジェクトの配列型になるので、[]を付与しました。

 * @property NewsTopImage|null $HeadImage
 * @property NewsTopImage[] $Image

全体のlevel底上げと新規コードに厳格ルール適応

PHPStanで検知してくれるlevelごとの内容は下記です。

0.基本的なチェック、不明なクラス、不明な関数、呼び出された不明な$thisメソッド、
 それらのメソッドと関数に渡された引数の数が間違っている、常に未定義の変数
1.おそらく未定義の変数、未知のマジックメソッドとプロパティ__call、__get
2.PHPDocsを検証する($thisだけでなく)すべての式で不明なメソッドをチェック
3.戻り値の型、プロパティに割り当てられた型
4.基本的なデッドコードチェック。
  常にfalse instanceofおよびその他の型チェック、デッドelseブランチ、戻り後の到達不能コード。等
5.メソッドと関数に渡された引数のタイプをチェック
6.タイプヒントの欠落を報告
7.部分的に間違った共用体型を報告する-共用体型の一部の型にのみ存在するメソッドを呼び出すと、レベル7がそれを報告し始めます。その他の誤った状況
8.メソッドの呼び出しとnull許容型のプロパティへのアクセスを報告する

このライブラリでPHPDocを付与することで、level 1の未知のプロパティへの検知が正確にできるようになりました。結果、修正が必要だったエラーは200弱で、修正後level 1を適用します。

しかし、level 2で実行すると、3000超のエラーが検知されてしまいました。PHPDocが正確に書かれていないケースが多いのと、他の一部マジックメソッドの補完が不十分な誤検知のためです。

PHPDocが間違っている数百のエラーを、手動で直すのは厳しいです。ただ検知できるということは、機械的な修正もある程度可能なはず。今回のように、PHP-Parserでのプログラムでの修正を試していくつもりです。

一方で、新規開発も進んでいるので、全体のlevelをしている間に、エラーが増える可能性もあります。

PHPStanには、こんな状況にぴったりの「新規コードにのみ厳格なルールを当てる」ベースラインという機能があります。既存のエラーのリストを作成し無視対象として設定することで、新規追加のみエラー検知します。

この機能を使用し、静的解析が正しく解釈できるようにPHPDocや拡張で調整しつつ、次は新規コードに焦点をあてようと思います。

#弁護士ドットコム #PHPStan #PHP #静的解析

この記事が気に入ったらサポートをしてみませんか?