今週は仕事のメインリポジトリの
— sasezaki (@sasezaki) March 27, 2020
phpstanならびにphpstan-doctrineをやっと^0.12に上げたので気が強い
はじめに
弊社のサーバーサイド でのメインプロジェクトでは、過去のブログエントリにもあるように、PHP ならびにORMとしてDoctrine を導入しています。
- PHPStan で Doctrine Criteria で使ってるフィールドを検証できるようにした - OTOBANK Engineering Blog
そして PHPStan をQAでの主な静的解析として利用しており、コードレビュー時の負担を減らすため機械が指摘できることは極力機械で行えるように随時設定の見直し・チェック項目の追加などを行っています。
先月にはプロジェクトでの導入している PHPStan ならびにそのエクステンションのひとつである phpstan-doctrine のバージョンを ^0.12 にやっとのことでアップデートをしました。 その際の対応点と、バージョンをあげたことによる恩恵などについて記したいと思います。
PHPStan 0.12 ならびに phpstan-doctrine とは
PHPStanはよりfeatureを盛り込んだ0.12.0
が 去年12月に リリースされていました。
- PHPStan 0.12 Released! - Ondřej Mirtes - Medium
- PHPStan 0.12.0がリリースされました - 超PHPerになろう
とりわけ ジェネリクスのサポートは Doctrineのコレクションクラスを多用しているプロジェクトではより型を検査することに活躍できますね。 0.12 でのほかの feature としては以下が挙げられます。
“class-string” 擬似型 でのクラス文字列名のチェック
これは例えば
<?php /** * @param class-string $className */ function foo(string $className): void { } foo('DateTime'); foo(Bar::class); // Class Bar not found.
という風に、主にライブラリでクラス名を指定するようなケースでのミス防止につながります。
phpstan/phpdoc-parserが0.4 にあがったことによる array-shapes (Object-like arrays) へのチェック
https://t.co/lqi4W7mQgo pic.twitter.com/laxIVr3ido
— sasezaki (@sasezaki) August 1, 2019
※ 正直なところとしては psalmですでにサポートされている array-shapes記法が PHPStan:0.11 ではエラー(Unexpected token)となってムムムと思ってました。
「PHPでの型」と 「Doctrineでのカラム型」の不一致の判定
上述の OndrejMirtesのブログエントリ内にある 「Doctrine extension: compare property type against @ORM\Column definition」の記述の通り、Doctrineで定義されている型と@var
アノテーションでのプロパティ型に不一致がある場合警告してくれるようになりました。これは ^0.12
に上げることで最も大きい恩恵と言えるでしょう。
これは主に、nullable=true
とカラムを定義していたにも関わらず、@var
アノテーションにnullableなユニオン型チェックを定義していない検出に役立ちました。
また、下記のようなdeciaml
定義をしたにも関わらず int を記述しているようなケースについても
/**
* @var int
* @ORM\Column(name="latitude", type="decimal")
*/
private $latitude;
type mapping mismatch\\: database can contain string but property expects int
と警告で気づけるようになりました。
また、
<?php class User { /** * @var CreditCard * * @ORM\OneToOne(targetEntity="CreditCard", mappedBy="user", cascade={"persist","remove"}) */ private $creditCard;
のような nullがありうる OneToOneなどのアソシエーションへも
------ ---------------------------------------------------------------------------------------------------------- Line User.php ------ ---------------------------------------------------------------------------------------------------------- 123 Property User::$creditCard type mapping mismatch: database can contain CreditCard|null but property expects CreditCard. ------ ----------------------------------------------------------------------------------------------------------
と警告がでるようになりました。
^0.12 に上げるためにしたこと、併せて行ったこと
otobank/phpstan-doctrine-criteria をアップデート
PHPStan 0.12 への変更に追従しているかを確証するために各ユニットテストの追加と 依存する phpstanならびにphpstan-doctrineのバージョンを 0.12 系に上げました。 https://github.com/otobank/phpstan-doctrine-criteria/pull/1
不足していたプロパティ指定の追加。
phpstan-doctrineを 0.12 に上げた際に、主に対応量が多かったのは、下記のような OneToManyなプロパティについて、
<?php class Audiobook { /** * @var \Doctrine\Common\Collections\Collection * * @ORM\OneToMany(targetEntity="Artwork", mappedBy="audiobook", cascade={"persist","remove"}, orphanRemoval=true) */ private $artworks;
------ ----------------------------------------------------------------------------------------------------------- Line Audiobook.php ------ ----------------------------------------------------------------------------------------------------------- 217 Property Audiobook::$artworks type mapping mismatch: property can contain Doctrine\Common\Collections\Collection but database expects Doctrine\Common\Collections\Collection&iterable<Artwork>. ------ -----------------------------------------------------------------------------------------------------------
と警告が出る点でした。 これはもちろんそのプロパティを参照するさいに(foreachで回す場合など)はどのエンテイティを取得しているか必要となるため、これは警告通り iterable<エンテイティクラス>なユニオン指定を地道に追加していきました。
<?php class Audiobook { /** * @var \Doctrine\Common\Collections\Collection&iterable<Artwork> * * @ORM\OneToMany(targetEntity="Artwork", mappedBy="audiobook", cascade={"persist","remove"}, orphanRemoval=true) */ private $artworks;
プロパティタイプヒントを必須に
That should still be declared via `var` docblock annotation.
— Can't fix stupid, can't quarantine it either. (@Ocramius) October 11, 2018
上述の"「PHPでの型」と 「Doctrineでのカラム型」の不一致の判定"は、各プロパティの型が検出できなければいけません。PHPStan でのlevel 6 では(主にアノテーションでの)タイプヒントに関しての各ルールが追加され、この中の MissingPropertyTypehintRule
によってタイプヒント指定されているかを検出できます。
また、phpstan.neonでのパラメータで inferPrivatePropertyTypeFromConstructorをtrueにすればコンストラクタでの型指定からプロパティの型補足する方法もあります。 しかしながら、現在のコードベースでのタイプヒント全般については、継続的に都度記述を行って改善している段階であります。
そのため、プロパティタイプでの型宣言を必須とするチェックには、PHP_CodeSniffer 用の追加ルール slevomat/coding-standard での SlevomatCodingStandard.TypeHints.PropertyTypeHint
Sniff を用いて コーディング規約チェックとして検出するようにしました。
まとめ
この記事では、PHPStan 0.12 を導入した点でも特に、phpstan-doctrine
での利点/対応点についてふれました。
ここ最近の PHP静的解析シーンでの注目は、強力な擬似型のサポートであるわけですが、今回メインプロジェクトにて PHPStanを0.12に上げたことにより、その効果によりバグの減少やテストの強化につながる土台を整えらえて安堵しています。