Slide 1

Slide 1 text

巨大なモノリスの静的解析を
 レベルMaxにする方法
 2021/10/03


Slide 2

Slide 2 text

自己紹介
 ● 古賀 敦士
 ● 株式会社ホワイトプラス所属
 ● Webアプリケーション開発、PHP基盤改善
 ● メインはPHP, 時々Go, たまにTypeScript


Slide 3

Slide 3 text


 ネット型 宅配クリーニング


Slide 4

Slide 4 text

今日話すこと
 1. PHPStan静的解析の基盤を整える
 2. 静的解析レベルをMAXに上げる


Slide 5

Slide 5 text

今日話すこと
 1. PHPStan静的解析の基盤を整える
 2. 静的解析レベルをMAXに上げる


Slide 6

Slide 6 text

LenetのPHPコードベース
 PHP 7.4
 Laravel 6.x
 モジュラーモノリス
 10年以上稼働
 (レガシーコード多め)


Slide 7

Slide 7 text

静的解析(PHPStan)の概要
 PHPStan: 0.12.25(最新: 0.12.99)
 larastan: 0.5.7(最新: 0.7.12)
 解析対象パス: app, controllers, lib
 解析レベル: Lev.1 


Slide 8

Slide 8 text

PHPStanレベル別主な検査内容
 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 


Slide 9

Slide 9 text

LenetのPHPStan適用範囲
 0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 ごく一部の検査しかされていない


Slide 10

Slide 10 text

・PHPStan静的解析
 ・ユニットテスト
 ・コードレビュー
 ・手動テスト
 Lenetのコード検証方法
 頑張る
 弱い


Slide 11

Slide 11 text

・PHPStan静的解析
 ・ユニットテスト
 ・コードレビュー
 ・手動テスト
 Lenetのコード検証方法
 頑張る
 弱い
 つらい
 漏れる


Slide 12

Slide 12 text

静的解析レベルを上げる💪


Slide 13

Slide 13 text

Lev.1 -> Lev.2 に上げると...
 色んな場所で大量にエラーが出る → 一度に直すには大変 ×
 ×
 ×
 ×
 ×
 ×


Slide 14

Slide 14 text

Lev.1 -> Lev.2 に上げると...
 モジュール単位で担当チームが存在 → 調整が必要 色んな場所で大量にエラーが出る → 一度に直すには大変 ×
 ×
 ×
 ×
 ×
 ×


Slide 15

Slide 15 text

Lev.1 -> Lev.2 に上げると...
 モジュール単位で担当チームが存在 → 調整が必要 普段触らないところは修正しづらい 色んな場所で大量にエラーが出る → 一度に直すには大変 ×
 ×
 ×
 ×
 ×
 ×


Slide 16

Slide 16 text

Lev.1 -> Lev.2 に上げると...
 モジュール単位で担当チームが存在 → 調整が必要 普段触らないところは修正しづらい 色んな場所で大量にエラーが出る → 一度に直すには大変 ×
 ×
 ×
 ×
 ×
 ×
 レベルを上げられない

Slide 17

Slide 17 text

Lev.1 -> Lev.MAX に上げると...
 集中突破できない😦

Slide 18

Slide 18 text

放置している間も負債は増える


Slide 19

Slide 19 text

他と同じくLev.1にしなければならない
 新しいモジュールも同じレベル
 New


Slide 20

Slide 20 text

何とかしたい!


Slide 21

Slide 21 text

色んなモジュールでエラー発生
 ↓
 エラー量が多い
 チーム間調整が必要
 知識が乏しいところは修正しづらい
 ↓
 レベルを上げられない
 レベル上げの課題整理
 ×
 ×
 ×
 ×
 Lev.1 -> 2


Slide 22

Slide 22 text

特定モジュールでのみエラー発生
 ↓
 エラー数が抑えられる
 チーム間調整不要
 得意・必要な領域から対応
 ↓
 レベル上げに着手できる!
 もしモジュール単位でレベル上げできれば
 ×
 Lev.1 -> 2


Slide 23

Slide 23 text

モジュール(ディレクトリ)毎に
 静的解析レベルを切り替えるには?


Slide 24

Slide 24 text

1回のPHPStan 実行では1つのレベルしか設定できない仕様
 複数レベルで実行するには複数回実行しなければならない
 
 Issues: https://github.com/phpstan/phpstan/issues/705


Slide 25

Slide 25 text

設定ファイルを分割する案
 PHPStanをレベルの数だけ実行
 phpstan analyse -c phpstan-level1.neon 
 phpstan analyse -c phpstan-level2.neon 
 共通設定
 phpstan-level1.neon
 level: 1
 paths: lib/Lenet/User
 includes
 レベル別設定
 phpstan-level2.neon
 level: 2
 paths: lib/Lenet/Cleaning
 phpstan.neon


Slide 26

Slide 26 text

共通設定
 phpstan-level1.neon
 level: 1
 paths: lib/Lenet/User
 includes
 レベル別設定
 PHPStanをレベルの数だけ実行
 phpstan-level2.neon
 level: 2
 paths: lib/Lenet/Cleaning
 phpstan.neon
 phpstan analyse -c phpstan-level1.neon 
 phpstan analyse -c phpstan-level2.neon 
 👍 シンプル
 👎 masterとの差分実行ができない
 不採用 設定ファイルを分割する案


Slide 27

Slide 27 text

差分実行の仕組み
 lib/Finance/File1.php
 lib/Lenet/Cleaning/File1.php
 - level: 1
 paths: lib/Finance
 - level: 2
 paths: lib/Lenet/Cleaning
 照合ロジック
 Lev.1: lib/Finance/File1.php
 Lev.2: lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Finance/File1.php 
 phpstan analyse -c phpstan.neon --level=2 lib/Lenet/Cleaning/File1.php 
 差分
 対応表
 git diff でmasterと開発ブランチの 差分ファイルを出力


Slide 28

Slide 28 text

差分実行コマンドを作成
 lib/Finance/File1.php
 lib/Lenet/Cleaning/File1.php
 - level: 1
 paths: lib/Finance
 - level: 2
 paths: lib/Lenet/Cleaning
 照合ロジック
 Lev.1: lib/Finance/File1.php
 Lev.2: lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Finance/File1.php 
 phpstan analyse -c phpstan.neon --level=2 lib/Lenet/Cleaning/File1.php 
 対応表
 PHPStanClient
 (artisan phpstan-client --diff)
 差分


Slide 29

Slide 29 text

(artisan phpstan-client)
 全ファイル静的解析実行時
 - level: 1
 paths: lib/Finance
 - level: 2
 paths: lib/Lenet/Cleaning
 phpstan analyse -c phpstan.neon --level=1 lib/Finance
 phpstan analyse -c phpstan.neon --level=2 lib/Lenet/Cleaning
 対応表
 対応表を読み込んで レベル分実行する
 PHPStanClient


Slide 30

Slide 30 text

新しいモジュールを切った時はどうなる?


Slide 31

Slide 31 text

新しいモジュールの設定
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths: lib/Lenet/Alliance
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 追加しないといけない 
 PHPStanClient
 (artisan phpstan-client --diff)
 差分


Slide 32

Slide 32 text

新しいモジュールの設定
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths: lib/Lenet/Alliance
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 追加しないといけない 
 漏れそう 設定知識が必要 PHPStanClient
 (artisan phpstan-client --diff)
 差分
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php


Slide 33

Slide 33 text

業務モジュールはデフォルトでLev.MAX
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths: lib
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 複数のレベルが
 ヒットしたら
 低い方を適用
 PHPStanClient
 (artisan phpstan-client --diff)
 差分
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php


Slide 34

Slide 34 text

新しいモジュールのデフォルトはLev.MAX
 - level: 2
 paths: lib/Lenet/Cleaning
 - level: 8
 paths: lib
 照合ロジック
 Lev.2: lib/Lenet/Cleaning/File1.php 
 Lev.8: lib/Lenet/Alliance/File1.php 
 phpstan analyse -c phpstan.neon --level=1 lib/Lenet/Cleaning/File1.php 
 phpstan analyse -c phpstan.neon --level=8 lib/Lenet/Alliance/File1.php 
 対応表
 複数のレベルが
 ヒットしたら
 低い方を適用
 PHPStanClient
 (artisan phpstan-client --diff)
 設定不要 開発に集中できる 差分
 lib/Lenet/Alliance/File1.php
 lib/Lenet/Cleaning/File1.php


Slide 35

Slide 35 text

これまでのまとめ
 解析対象パスとレベルの紐付けを共通設定から切り出し、
 照合ロジックを持った解析実行コマンドを作成し実行する
 モジュール毎に設定したレベルで静的解析可能になり、
 独立してレベル上げできるようになった🙌


Slide 36

Slide 36 text

けど本当にレベルMAXまで到達できるか?


Slide 37

Slide 37 text

今日話すこと
 1. PHPStan静的解析の基盤を整える
 2. 静的解析レベルをMAXに上げる


Slide 38

Slide 38 text

まずはライブラリバージョンアップ
 PHPStan: 0.12.25 -> 0.12.90
 larastan: 0.5.7 -> 0.7.8
 これをやらないと既バグを踏む可能性がある
 当時の最新 実際に踏みました󰢧


Slide 39

Slide 39 text

0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 レベル上げのロードマップ


Slide 40

Slide 40 text

0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 Lenet でのレベル上げのポイント
 
 


Slide 41

Slide 41 text

Lev.4
 デッドコードチェック


Slide 42

Slide 42 text

デッドコードチェックとは


Slide 43

Slide 43 text

always false デッドコードチェックとは


Slide 44

Slide 44 text

デッドコードを 削除 デッドコードチェックとは


Slide 45

Slide 45 text

やっかいなケースに遭遇


Slide 46

Slide 46 text

Eloquentで所有元に
 外部キーなしでアクセスする


Slide 47

Slide 47 text

所有元に外部キー無しでアクセス
 外部キー無き参照
 存在チェックが必要


Slide 48

Slide 48 text

所有元に外部キー無しでアクセス
 外部キー無き参照
 error: プロパティが存在しない


Slide 49

Slide 49 text

laravel-ide-helper でPHPDocを付与


Slide 50

Slide 50 text

Eloquentで所有元にアクセスする
 追加された


Slide 51

Slide 51 text

Eloquentで所有元にアクセスする
 nullable ではない error: always false
 (デッドコード判定)


Slide 52

Slide 52 text

Eloquentで所有元にアクセスする
 外部キーがない場合は User|null であるべき → ide-helper の課題 error: always false
 (デッドコード判定)


Slide 53

Slide 53 text

laravel-ide-helperを修正しました
 https://github.com/barryvdh/laravel-ide-helper/pull/1231 


Slide 54

Slide 54 text

修正バージョンは未リリース(2021/10/1 時点)
 暫定的にide-helperを直接拡張し修正を施して対応


Slide 55

Slide 55 text

Eloquentで所有元にアクセスする
 修正された!

Slide 56

Slide 56 text

同じ問題に遭遇した場合は、
 ・ ide-helper の修正バージョンリリース後に取り込む
  or
 ・ide-helper を拡張し修正する
 の対応を検討してみてください


Slide 57

Slide 57 text

Lev.6
 欠落した型宣言


Slide 58

Slide 58 text

型宣言の強制


Slide 59

Slide 59 text

型宣言の強制


Slide 60

Slide 60 text

プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 型宣言の強制


Slide 61

Slide 61 text

宣言漏れ
 プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 型宣言の強制


Slide 62

Slide 62 text

プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 宣言漏れ
 全部で 731 errors
 型宣言の強制


Slide 63

Slide 63 text

全部で 731 errors
 全てに型を導入する には時間がかかる
 プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 宣言漏れ
 型宣言の強制


Slide 64

Slide 64 text

Baseline で回避
 全てに型を導入する には時間がかかる
 プロパティ型宣言は PHP7.4 で導入
 未対応のコードが多い
 宣言漏れ
 全部で 731 errors
 型宣言の強制


Slide 65

Slide 65 text

Baseline とは
 検出したエラーの一覧をファイルに出力し
 それを読み込むことにより
 エラーをまとめて無視する


Slide 66

Slide 66 text

Baseline を使用するケース
 ・より高いレベルに上げたい
 ・PHPStanバージョンアップ
 ・カスタムルールの導入
 これらによって、数十〜数百くらいのエラーが出るが対処する時間・労力が無い場 合に最適
 今回


Slide 67

Slide 67 text

Baseline 生成・読み込み


Slide 68

Slide 68 text

欠落した型宣言のチェック


Slide 69

Slide 69 text

欠落した型宣言のチェック
 既知のエラーとなり 報告しない

Slide 70

Slide 70 text

レベル上げ完了!


Slide 71

Slide 71 text

レベルMAXで運用した感想
 ・型を強制し整合性をチェックすることで一定の品質を保てるようになった
 ・検査が厳格になったことでCIエラーになる頻度が増えた
  → ローカル環境で差分実行を行って頻繁にチェック
 ・対応に悩むエラーに時々遭遇
  → 解決したものをwikiで共有
 ・Baselineで無視したエラーの扱い
  → 継続的なリファクタで残し続けないように


Slide 72

Slide 72 text

ご静聴ありがとうございました


Slide 73

Slide 73 text

付録


Slide 74

Slide 74 text

0: 未知のクラス・関数、 thisで呼ばれる未知のメソッド、メソッドや関数に渡される引数の数 
 1: 未定義の変数、未知のマジックメソッド、__call および __get を持つクラスのプロパティ 
 2: 未知のメソッド、PHPDocバリデーション 
 3: 戻り値の型、プロパティに割り当てられた型 
 4: デッドコードチェック 
 5: メソッドや関数に渡される引数の型 
 6: 欠落した型宣言
 7: 部分的に間違った直和型の検査 
 8: nullable型のメソッド呼び出しやプロパティアクセス 
 レベル上げのロードマップ
 
 
 
 
 
 発表で説明できなかったレベル別の ルールについて簡単に紹介します!

Slide 75

Slide 75 text

Lev.1 -> Lev.2


Slide 76

Slide 76 text

未定義のメソッドチェック


Slide 77

Slide 77 text

未定義のメソッドチェック
 
 未定義

Slide 78

Slide 78 text

未定義のメソッドチェック
 
 未定義の メソッドを削除

Slide 79

Slide 79 text

未定義のプロパティ


Slide 80

Slide 80 text

未定義のプロパティ
 動的プロパティ 未定義と判定される

Slide 81

Slide 81 text

未定義のプロパティ
 mixed で逃げる

Slide 82

Slide 82 text

未定義のプロパティ
 外に切り出して total を持つクラス を戻り値型として定 義して使う

Slide 83

Slide 83 text

PHPDoc バリデーション


Slide 84

Slide 84 text

PHPDoc バリデーション
 型定義が無い

Slide 85

Slide 85 text

PHPDoc バリデーション
 型を定義

Slide 86

Slide 86 text

PHPDoc バリデーション
 型をつけられない時 はmixed

Slide 87

Slide 87 text

Lev.2 -> Lev.3


Slide 88

Slide 88 text

戻り値型チェック


Slide 89

Slide 89 text

戻り値型チェック
 型不一致

Slide 90

Slide 90 text

戻り値型チェック
 型を一致させる

Slide 91

Slide 91 text

プロパティ代入時の型チェック


Slide 92

Slide 92 text

プロパティ代入時の型チェック
 型不一致

Slide 93

Slide 93 text

プロパティ代入時の型チェック
 型を一致

Slide 94

Slide 94 text

Lev.4 -> Lev.5


Slide 95

Slide 95 text

メソッド引数の型チェック


Slide 96

Slide 96 text

メソッド引数の型チェック
 型不一致

Slide 97

Slide 97 text

メソッド引数の型チェック
 型を一致

Slide 98

Slide 98 text

Lev.6 -> Lev.7


Slide 99

Slide 99 text

直和型のチェック


Slide 100

Slide 100 text

直和型のチェック


Slide 101

Slide 101 text

直和型のチェック
 不一致


Slide 102

Slide 102 text

直和型のチェック
 異常系をハンドリング

Slide 103

Slide 103 text

Lev.7 -> Lev.8(MAX)


Slide 104

Slide 104 text

nullable型のチェック


Slide 105

Slide 105 text

nullable型のチェック
 nullable型の メソッド呼び出し nullable型でない

Slide 106

Slide 106 text

nullable型のチェック