Upgrade to Pro — share decks privately, control downloads, hide ads and more …

20年もののレガシープロダクトに 0からPHPStanを入れるまで / phpcon2024

hirobe
December 20, 2024

20年もののレガシープロダクトに 0からPHPStanを入れるまで / phpcon2024

hirobe

December 20, 2024
Tweet

More Decks by hirobe

Other Decks in Programming

Transcript

  1. 6 MailDealerが抱えていた課題 • コード品質担保において、機械的な対処をなにもしていない ◦ 実装者や、コードレビュアーが⼈⼒で頑張るしかない ◦ 特にオフショア担当分のコードレビューは 指摘も多くなりレビュアーの負担が⼤きい •

    新しい技術的負債を作りやすい環境になってしまっている ◦ PhpStormのInspectionはあるが、既存コードが 警告だらけで、新しく警告が出ても気付けない ◦ PHPのerror_reportingも致命的なエラー表⽰のみ
  2. 9 PHPStan • PHP⽤の静的解析ツール • コードを解析し、未定義変数やデッドコード、型の不⼀致 などを⾒つけてくれる • baselineという機能がレガシーコードと相性がいい ◦

    既存のエラーを無視して、新しいコードにのみ エラー報告を⾏ってくれる。 • 11/11にPHPStan2.0がリリースされた(今回は1系の話です)
  3. 13 原因 • Getting Startedの⽅法に従って、CLIで実⾏した ◦ vendor/bin/phpstan analyse {ディレクトリ} •

    MailDealerはフレームワークを導⼊しておらず、 オリジナリティあふれるディレクトリ構造をしている ◦ ⼀般的な指定⽅法では、ライブラリ等も まとめて解析されてしまう • includeしているファイルの拡張⼦を.inc にしているため 解析対象にならなかった(初期値は .php のみ)
  4. エラー分析 Reached internal errors count limit of 50, exiting… Internal

    error: Internal error: Class "Hoge" not found while analysing file このようなエラーが⼤量に出ていた Hoge Classが読み込まれていない……? 26
  5. 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
  6. 出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • bootstrapFilesに指定したファイルは ”PHPランタイムによって実⾏される”

    • 解析を実⾏した環境でも、 実際にrequireできるPathが存在しないといけない • MailDealerは、個⼈の仮想環境にデプロイして動作確認をする 解析を実⾏した開発環境ではrequire先が存在しなかった 38
  7. 出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • 解決案1 ◦

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

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

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

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

    幸いにも、requireパスは定数で指定してあった ◦ require LIBPATH . ‘hoge.inc’ • bootstrapFiles内で、定数を上書きして 開発環境のパスでrequireを⾏うようにした ◦ この作業中、定数を使っていないrequireを発⾒しました…… 44
  12. 出ていたエラーの例② Call to a member function get() on null •

    謎 • 当該オブジェクト作成処理が冗⻑で、 PHPStanの解析上なんらかの問題があった? 45
  13. $object = createObject(); function createObject() { global $object; if (is_null($object))

    { $_object = new SampleObject(); } else { return $object; } return $_object; } 46
  14. 出ていたエラーの例② Call to a member function get() on null •

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

    謎 • 当該オブジェクト作成処理がかなり冗⻑で、 PHPStanの解析上なんらかの問題があった? • リファクタリングしたらエラーが出なくなった • もしわかる⽅いれば…… 49
  17. おまけ(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";
  18. エラー数 • レベル0で759 errors ◦ エラー内容をみても、妥当そう • レベル1で68,374 errors ◦

    レベル1から、条件によっては未定義になる変数 をチェックしてくれる ◦ 悲しいかな、これも正しいエラーの可能性が⾼い ◦ これ以上レベルを上げても、爆発的には増えなかった 56
  19. baselineでも無視できないエラーが存在する Unignorable could not be added to the baseline: Cannot

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