
静的解析のために、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や拡張で調整しつつ、次は新規コードに焦点をあてようと思います。
気軽にクリエイターの支援と、記事のオススメができます!