Slide 1

Slide 1 text

© RAKUS Co., Ltd. 20年もののレガシープロダクトに 0からPHPStanを⼊れるまで #phpcon 2024/12/22 廣部 知⽣(@tomoki2135)

Slide 2

Slide 2 text

2 ⾃⼰紹介 21卒で株式会社ラクスに⼊社 PHPでMail Dealerの開発を⾏っています レガシープロダクトの改善について考える⽇々 趣味はスト6で戦いの螺旋を登り続けています (Act5はMR1700でフィニッシュ)

Slide 3

Slide 3 text

3           について メール共有管理システム 15年連続シェアNo.1(2009〜2023)※ 歴史は更に⻑く、2001年4⽉に販売開始 Laravelは2011年リリースなので、10歳年上 ※出典:ITR「ITR Market View:メール∕Webマーケティング市場2024」  メール処理市場:ベンダー別売上⾦額推移およびシェア2009-2023年度(予測値)

Slide 4

Slide 4 text

© RAKUS Co., Ltd. 4 Q.なぜこんなレガシープロダクトに PHPStanを?

Slide 5

Slide 5 text

© RAKUS Co., Ltd. 5 A.コード品質担保のため

Slide 6

Slide 6 text

6 MailDealerが抱えていた課題 ● コード品質担保において、機械的な対処をなにもしていない ○ 実装者や、コードレビュアーが⼈⼒で頑張るしかない ○ 特にオフショア担当分のコードレビューは 指摘も多くなりレビュアーの負担が⼤きい ● 新しい技術的負債を作りやすい環境になってしまっている ○ PhpStormのInspectionはあるが、既存コードが 警告だらけで、新しく警告が出ても気付けない ○ PHPのerror_reportingも致命的なエラー表⽰のみ

Slide 7

Slide 7 text

© RAKUS Co., Ltd. 7 このままの環境で開発を続けても コード品質は良くならない

Slide 8

Slide 8 text

© RAKUS Co., Ltd. PHPStan導⼊で 少しでも環境を改善したい! 8

Slide 9

Slide 9 text

9 PHPStan ● PHP⽤の静的解析ツール ● コードを解析し、未定義変数やデッドコード、型の不⼀致 などを⾒つけてくれる ● baselineという機能がレガシーコードと相性がいい ○ 既存のエラーを無視して、新しいコードにのみ エラー報告を⾏ってくれる。 ● 11/11にPHPStan2.0がリリースされた(今回は1系の話です)

Slide 10

Slide 10 text

10 なぜPHPStanなのか? ● PHPの静的解析ツールのデファクトスタンダード (だと思ってる) ● MailDealer以外の弊社PHPプロダクトでは、 PHPStanを導⼊している ○ プロダクト間のノウハウ共有がしやすい

Slide 11

Slide 11 text

© RAKUS Co., Ltd. いざ初回実⾏! 11

Slide 12

Slide 12 text

© RAKUS Co., Ltd. するもうまく解析できていない…… 12

Slide 13

Slide 13 text

13 原因 ● Getting Startedの⽅法に従って、CLIで実⾏した ○ vendor/bin/phpstan analyse {ディレクトリ} ● MailDealerはフレームワークを導⼊しておらず、 オリジナリティあふれるディレクトリ構造をしている ○ ⼀般的な指定⽅法では、ライブラリ等も まとめて解析されてしまう ● includeしているファイルの拡張⼦を.inc にしているため 解析対象にならなかった(初期値は .php のみ)

Slide 14

Slide 14 text

© RAKUS Co., Ltd. まずは設定から 14

Slide 15

Slide 15 text

PHPStanの設定 neonというyamlに似た形式で設定を記述できる 解析レベル、解析対象のファイル、解析対象外ファイル、 無視するエラー等、様々な設定ができる 詳細:https://phpstan.org/config-reference 今回は最低限の設定だけ紹介します 15

Slide 16

Slide 16 text

PHPStanの設定 解析レベル ● レベルが⾼いほど厳しくチェックされる ● 今回は最低限のレベル0で設定 ○ レベル0ですら⼤量のエラーがでることが予測される ○ まずはレベル0で導⼊して様⼦⾒がしたい ○ 実装担当のオフショアチームとの 丁寧なコミュニケーションが必要 16

Slide 17

Slide 17 text

PHPStanの設定 excludePaths ● 解析対象外のディレクトリを指定できる fileExtensions ● 解析対象の拡張⼦を指定できる。デフォルトは.phpのみ ● 前述した通り、MailDealerは.incファイルがあるので指定 17

Slide 18

Slide 18 text

ここで学んだこと ● 適切な設定をしてからPHPStanを実⾏すること ○ 解析レベル ○ 解析対象外のディレクトリ ○ 解析対象のファイル拡張⼦ は要注意! 18

Slide 19

Slide 19 text

© RAKUS Co., Ltd. いざ再実⾏! 19

Slide 20

Slide 20 text

© RAKUS Co., Ltd. レベル0で271エラー 20

Slide 21

Slide 21 text

© RAKUS Co., Ltd. 思ったより少ないな……🤔 21

Slide 22

Slide 22 text

© RAKUS Co., Ltd. ちょっとレベル上げてみるか…… 22

Slide 23

Slide 23 text

© RAKUS Co., Ltd. レベル9:29,842 errors! 正直、こんなもんか😊と思いました 23

Slide 24

Slide 24 text

© RAKUS Co., Ltd. レベル4:32,495 errors! ふ、増えてる…… 😱 24

Slide 25

Slide 25 text

© RAKUS Co., Ltd. そんなことありえるのか……? 正しく解析できてないのでは……? 25

Slide 26

Slide 26 text

エラー分析 Reached internal errors count limit of 50, exiting… Internal error: Internal error: Class "Hoge" not found while analysing file このようなエラーが⼤量に出ていた Hoge Classが読み込まれていない……? 26

Slide 27

Slide 27 text

PHPStanの特性 PHPStanは、composerのautoloadを⾃動で解析してくれる composerをインストールしていて、autoloadを採⽤していれば ほぼ設定せずに利⽤できる が……requireは読み込んでくれない! 27

Slide 28

Slide 28 text

MailDealerのアーキテクチャ maildealer ├web │├index.php │├top.php │└side.php └common  ├global.inc  └func.inc Apacheがweb配下をhtdocsとして読み込む それぞれのファイルが、func.incのような 共通したファイルをrequireで読み込みに⾏く 28

Slide 29

Slide 29 text

© RAKUS Co., Ltd. autoloadをほぼ使っていない! \(^o^)∕ ⼀応新しく作った箇所はautoload対応 している箇所もあります…… 29

Slide 30

Slide 30 text

PHPStanの特性 autoloadは⾃動で読み込んでくれるが、 requireは解析してくれない Reached internal errors count limit of 50, exiting... Internal error: Internal error: Class "Hoge" not found while analysing file このエラーが出ている Hoge もrequireで読み込んでいるファイル 30

Slide 31

Slide 31 text

requireを読み込むには bootstrapFilesという設定値が存在する bootstrapFilesに事前に読み込みたいファイルを指定すると、 解析前に読み込んでくれる ⾃作のautoloadもここに指定する 今回は、func.incのような汎⽤ファイルを読み込ませる 31

Slide 32

Slide 32 text

ここで学んだこと ● PHPStanは、composerのautoloadを解析してくれる ● ⾃前のautoloaderを利⽤している、 そもそもautoloadを使っていない場合は、 bootstrapFilesを利⽤して事前に読み込ませること 32

Slide 33

Slide 33 text

© RAKUS Co., Ltd. いざ再実⾏! 33

Slide 34

Slide 34 text

© RAKUS Co., Ltd. 解析すら実⾏されず、PHPエラー 34

Slide 35

Slide 35 text

bootstrapFilesの注意点 bootstrapFilesに指定したファイルは ”PHPランタイムによって実⾏される” 実⾏して問題ない形式でないとPHPエラーが発⽣してしまう 逆に⾔うと、PHPランタイムを実⾏してくれるので、 ある程度⾃由にbootstrapFilesを記述できる 35

Slide 36

Slide 36 text

© RAKUS Co., Ltd. まてよ…… 36

Slide 37

Slide 37 text

© RAKUS Co., Ltd. なんで普段動いているはずのPHPファイルで PHPエラーがでるんですか? 󰤇 37

Slide 38

Slide 38 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● bootstrapFilesに指定したファイルは ”PHPランタイムによって実⾏される” ● 解析を実⾏した環境でも、 実際にrequireできるPathが存在しないといけない ● MailDealerは、個⼈の仮想環境にデプロイして動作確認をする 解析を実⾏した開発環境ではrequire先が存在しなかった 38

Slide 39

Slide 39 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● 解決案1 ○ requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える ● 解決案2 ○ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する 39

Slide 40

Slide 40 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● 解決案1 ○ requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える ● 解決案2 ○ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する 採用 40

Slide 41

Slide 41 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● 解決案1 ○ requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える ● 解決案2 ○ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する したんですが…… 41

Slide 42

Slide 42 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● PHPStanは、シンボリックリンクを利⽤するときに 解析がバグるときがある https://github.com/phpstan/phpstan/issues/7241 ● シンボリックを採⽤すべきではないと判断 42

Slide 43

Slide 43 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● 解決案1 ○ requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える ● 解決案2 ○ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する 採用 43

Slide 44

Slide 44 text

出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' ● bootstrapFilesは、”PHPランタイムによって実⾏される” ● 幸いにも、requireパスは定数で指定してあった ○ require LIBPATH . ‘hoge.inc’ ● bootstrapFiles内で、定数を上書きして 開発環境のパスでrequireを⾏うようにした ○ この作業中、定数を使っていないrequireを発⾒しました…… 44

Slide 45

Slide 45 text

出ていたエラーの例② Call to a member function get() on null ● 謎 ● 当該オブジェクト作成処理が冗⻑で、 PHPStanの解析上なんらかの問題があった? 45

Slide 46

Slide 46 text

$object = createObject(); function createObject() { global $object; if (is_null($object)) { $_object = new SampleObject(); } else { return $object; } return $_object; } 46

Slide 47

Slide 47 text

出ていたエラーの例② Call to a member function get() on null ● 謎 ● 当該オブジェクト作成処理がかなり冗⻑で、 PHPStanの解析上なんらかの問題があった? ● リファクタリングしたらエラーが出なくなった 47

Slide 48

Slide 48 text

$object = createObject(); function createObject() { global $object; if (is_null($object)) { $_object = new SampleObject(); } else { return $object; } return $_object; } $object = createObject(); function createObject() { global $object; if (is_null($object)) { return new SampleObject(); } return $object; } 48

Slide 49

Slide 49 text

出ていたエラーの例② Call to a member function get() on null ● 謎 ● 当該オブジェクト作成処理がかなり冗⻑で、 PHPStanの解析上なんらかの問題があった? ● リファクタリングしたらエラーが出なくなった ● もしわかる⽅いれば…… 49

Slide 50

Slide 50 text

ここで学んだこと ● bootstrapFilesは、PHPランタイムで実⾏される ○ bootstrapFilesでは、PHPコードを記述できる ● bootstrapFilesは、PHPランタイムで実⾏できる ようにしなければならない 50

Slide 51

Slide 51 text

おまけ(bootstrapFilesの設定イメージ) 51 $global_hoge = "hoge"; $global_fuga = "fuga"; define("REQUIRE_PATH", "/usr/local/..."); require "REQUIRE_PATH" . "/sample.inc"; require "REQUIRE_PATH" . "/DB.php"; $db = new Db(); if (!$global_piyo) { require "REQUIRE_PATH" . "/piyo.inc";

Slide 52

Slide 52 text

© RAKUS Co., Ltd. 三度⽬の正直! (とはいいつつ、エラー解消のため何度もトライしてる) 52

Slide 53

Slide 53 text

© RAKUS Co., Ltd. 無事に解析できた! 53

Slide 54

Slide 54 text

© RAKUS Co., Ltd. レベル0で 759 errors 54

Slide 55

Slide 55 text

© RAKUS Co., Ltd. レベル1で 68,374 errors 55

Slide 56

Slide 56 text

エラー数 ● レベル0で759 errors ○ エラー内容をみても、妥当そう ● レベル1で68,374 errors ○ レベル1から、条件によっては未定義になる変数 をチェックしてくれる ○ 悲しいかな、これも正しいエラーの可能性が⾼い ○ これ以上レベルを上げても、爆発的には増えなかった 56

Slide 57

Slide 57 text

© RAKUS Co., Ltd. さすがに全部対応するのは現実的ではない 57

Slide 58

Slide 58 text

baseline 既存のエラーを無視し、新しいエラーのみ教えてくれる --generatebaseline というオプションをつけて解析すると baselineファイルを作成してくれる ファイル名の指定なしだと phpstan-baseline.neon が⽣成される ファイル名を指定して .php ファイルにしたほうが、 解析パフォーマンスが上がるらしい 58

Slide 59

Slide 59 text

© RAKUS Co., Ltd. baselineを作成して再実⾏! エラーは出ない想定 59

Slide 60

Slide 60 text

© RAKUS Co., Ltd. がっ、ダメ……! 出る……!エラーが……!まだ……! 60

Slide 61

Slide 61 text

baselineでも無視できないエラーが存在する Unignorable could not be added to the baseline: Cannot use [] for reading. 以下のようなコードがあったとき、PHPエラーにはならないが、 PHPStanでは無視できないエラーとして扱われる $arr[] .= ""; Issueでも修正しないと⾔われてしまっているので、 コード修正しかない 61

Slide 62

Slide 62 text

© RAKUS Co., Ltd. 頑張って修正していきます 62

Slide 63

Slide 63 text

PHPStanを導⼊してみて 出ているエラーには納得感がある 新規で未定義変数が新たに増えないだけでも品質が上がりそう PHP9では未定義変数が致命的なエラーになる予定なので PHPStanを活⽤すれば合わせて修正が進みそう CI導⼊も進めていく 63

Slide 64

Slide 64 text

まとめ レガシープロダクトでも、適切に設定することで PHPStanで静的解析が実⾏できる baselineを導⼊し、まずは新規コードから品質を担保する コード品質を諦めない!まずは⼀歩踏み出してみる 次回、運⽤編へ続く…… 64