Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
20年もののレガシープロダクトに 0からPHPStanを入れるまで / phpcon2024
Search
hirobe
December 20, 2024
Programming
0
870
20年もののレガシープロダクトに 0からPHPStanを入れるまで / phpcon2024
hirobe
December 20, 2024
Tweet
Share
More Decks by hirobe
See All by hirobe
PHPでOfficeファイルを取り扱う! PHP Officeライブラリを プロダクトに組み込んだ話
hirobe1999
0
1.8k
PHP8.1で、リソースがオブジェクトに!? マイナーリリースの変更が レガシープロダクトに与えた影響
hirobe1999
0
1.2k
フレームワークが存在しない時代からのレガシープロダクトを、 Laravelに”載せる”実装戦略
hirobe1999
0
1.3k
フレームワークが存在しない時代からのレガシープロダクトを、 Laravelに”載せる”実装戦略
hirobe1999
0
1.6k
新卒PHPer奮闘記 ~配属されたのは3歳違いのプロダクト!?~ / phperkaigi-2022-lt
hirobe1999
0
1.4k
Other Decks in Programming
See All in Programming
コンテナをたくさん詰め込んだシステムとランタイムの変化
makihiro
1
160
CloudflareStack でRAGに入門
asahiiwm
0
130
KubeCon + CloudNativeCon NA 2024 Overviewat Kubernetes Meetup Tokyo #68 / amsy810_k8sjp68
masayaaoyama
0
270
Monixと常駐プログラムの勘どころ / Scalaわいわい勉強会 #4
stoneream
0
300
DevFest - Serverless 101 with Google Cloud Functions
tunmise
0
130
menu基盤チームによるGoogle Cloudの活用事例~Application Integration, Cloud Tasks編~
yoshifumi_ishikura
0
110
20241217 競争力強化とビジネス価値創出への挑戦:モノタロウのシステムモダナイズ、開発組織の進化と今後の展望
monotaro
PRO
0
110
毎日13時間もかかるバッチ処理をたった3日で60%短縮するためにやったこと
sho_ssk_
1
410
オニオンアーキテクチャを使って、 Unityと.NETでコードを共有する
soi013
0
250
PHPで作るWebSocketサーバー ~リアクティブなアプリケーションを知るために~ / WebSocket Server in PHP - To know reactive applications
seike460
PRO
2
670
Cloudflare MCP ServerでClaude Desktop からWeb APIを構築
kutakutat
1
590
rails statsで大解剖 🔍 “B/43流” のRailsの育て方を歴史とともに振り返ります
shoheimitani
2
970
Featured
See All Featured
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
44
6.9k
GitHub's CSS Performance
jonrohan
1031
460k
The World Runs on Bad Software
bkeepers
PRO
66
11k
Speed Design
sergeychernyshev
25
680
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
29
2.3k
Building Adaptive Systems
keathley
38
2.3k
Designing Experiences People Love
moore
138
23k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
45
2.2k
Bash Introduction
62gerente
609
210k
Music & Morning Musume
bryan
46
6.2k
What’s in a name? Adding method to the madness
productmarketing
PRO
22
3.2k
StorybookのUI Testing Handbookを読んだ
zakiyama
28
5.4k
Transcript
© RAKUS Co., Ltd. 20年もののレガシープロダクトに 0からPHPStanを⼊れるまで #phpcon 2024/12/22 廣部 知⽣(@tomoki2135)
2 ⾃⼰紹介 21卒で株式会社ラクスに⼊社 PHPでMail Dealerの開発を⾏っています レガシープロダクトの改善について考える⽇々 趣味はスト6で戦いの螺旋を登り続けています (Act5はMR1700でフィニッシュ)
3 について メール共有管理システム 15年連続シェアNo.1(2009〜2023)※ 歴史は更に⻑く、2001年4⽉に販売開始 Laravelは2011年リリースなので、10歳年上 ※出典:ITR「ITR Market View:メール∕Webマーケティング市場2024」
メール処理市場:ベンダー別売上⾦額推移およびシェア2009-2023年度(予測値)
© RAKUS Co., Ltd. 4 Q.なぜこんなレガシープロダクトに PHPStanを?
© RAKUS Co., Ltd. 5 A.コード品質担保のため
6 MailDealerが抱えていた課題 • コード品質担保において、機械的な対処をなにもしていない ◦ 実装者や、コードレビュアーが⼈⼒で頑張るしかない ◦ 特にオフショア担当分のコードレビューは 指摘も多くなりレビュアーの負担が⼤きい •
新しい技術的負債を作りやすい環境になってしまっている ◦ PhpStormのInspectionはあるが、既存コードが 警告だらけで、新しく警告が出ても気付けない ◦ PHPのerror_reportingも致命的なエラー表⽰のみ
© RAKUS Co., Ltd. 7 このままの環境で開発を続けても コード品質は良くならない
© RAKUS Co., Ltd. PHPStan導⼊で 少しでも環境を改善したい! 8
9 PHPStan • PHP⽤の静的解析ツール • コードを解析し、未定義変数やデッドコード、型の不⼀致 などを⾒つけてくれる • baselineという機能がレガシーコードと相性がいい ◦
既存のエラーを無視して、新しいコードにのみ エラー報告を⾏ってくれる。 • 11/11にPHPStan2.0がリリースされた(今回は1系の話です)
10 なぜPHPStanなのか? • PHPの静的解析ツールのデファクトスタンダード (だと思ってる) • MailDealer以外の弊社PHPプロダクトでは、 PHPStanを導⼊している ◦ プロダクト間のノウハウ共有がしやすい
© RAKUS Co., Ltd. いざ初回実⾏! 11
© RAKUS Co., Ltd. するもうまく解析できていない…… 12
13 原因 • Getting Startedの⽅法に従って、CLIで実⾏した ◦ vendor/bin/phpstan analyse {ディレクトリ} •
MailDealerはフレームワークを導⼊しておらず、 オリジナリティあふれるディレクトリ構造をしている ◦ ⼀般的な指定⽅法では、ライブラリ等も まとめて解析されてしまう • includeしているファイルの拡張⼦を.inc にしているため 解析対象にならなかった(初期値は .php のみ)
© RAKUS Co., Ltd. まずは設定から 14
PHPStanの設定 neonというyamlに似た形式で設定を記述できる 解析レベル、解析対象のファイル、解析対象外ファイル、 無視するエラー等、様々な設定ができる 詳細:https://phpstan.org/config-reference 今回は最低限の設定だけ紹介します 15
PHPStanの設定 解析レベル • レベルが⾼いほど厳しくチェックされる • 今回は最低限のレベル0で設定 ◦ レベル0ですら⼤量のエラーがでることが予測される ◦ まずはレベル0で導⼊して様⼦⾒がしたい
◦ 実装担当のオフショアチームとの 丁寧なコミュニケーションが必要 16
PHPStanの設定 excludePaths • 解析対象外のディレクトリを指定できる fileExtensions • 解析対象の拡張⼦を指定できる。デフォルトは.phpのみ • 前述した通り、MailDealerは.incファイルがあるので指定 17
ここで学んだこと • 適切な設定をしてからPHPStanを実⾏すること ◦ 解析レベル ◦ 解析対象外のディレクトリ ◦ 解析対象のファイル拡張⼦ は要注意!
18
© RAKUS Co., Ltd. いざ再実⾏! 19
© RAKUS Co., Ltd. レベル0で271エラー 20
© RAKUS Co., Ltd. 思ったより少ないな……🤔 21
© RAKUS Co., Ltd. ちょっとレベル上げてみるか…… 22
© RAKUS Co., Ltd. レベル9:29,842 errors! 正直、こんなもんか😊と思いました 23
© RAKUS Co., Ltd. レベル4:32,495 errors! ふ、増えてる…… 😱 24
© RAKUS Co., Ltd. そんなことありえるのか……? 正しく解析できてないのでは……? 25
エラー分析 Reached internal errors count limit of 50, exiting… Internal
error: Internal error: Class "Hoge" not found while analysing file このようなエラーが⼤量に出ていた Hoge Classが読み込まれていない……? 26
PHPStanの特性 PHPStanは、composerのautoloadを⾃動で解析してくれる composerをインストールしていて、autoloadを採⽤していれば ほぼ設定せずに利⽤できる が……requireは読み込んでくれない! 27
MailDealerのアーキテクチャ maildealer ├web │├index.php │├top.php │└side.php └common ├global.inc └func.inc Apacheがweb配下をhtdocsとして読み込む
それぞれのファイルが、func.incのような 共通したファイルをrequireで読み込みに⾏く 28
© RAKUS Co., Ltd. autoloadをほぼ使っていない! \(^o^)∕ ⼀応新しく作った箇所はautoload対応 している箇所もあります…… 29
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
requireを読み込むには bootstrapFilesという設定値が存在する bootstrapFilesに事前に読み込みたいファイルを指定すると、 解析前に読み込んでくれる ⾃作のautoloadもここに指定する 今回は、func.incのような汎⽤ファイルを読み込ませる 31
ここで学んだこと • PHPStanは、composerのautoloadを解析してくれる • ⾃前のautoloaderを利⽤している、 そもそもautoloadを使っていない場合は、 bootstrapFilesを利⽤して事前に読み込ませること 32
© RAKUS Co., Ltd. いざ再実⾏! 33
© RAKUS Co., Ltd. 解析すら実⾏されず、PHPエラー 34
bootstrapFilesの注意点 bootstrapFilesに指定したファイルは ”PHPランタイムによって実⾏される” 実⾏して問題ない形式でないとPHPエラーが発⽣してしまう 逆に⾔うと、PHPランタイムを実⾏してくれるので、 ある程度⾃由にbootstrapFilesを記述できる 35
© RAKUS Co., Ltd. まてよ…… 36
© RAKUS Co., Ltd. なんで普段動いているはずのPHPファイルで PHPエラーがでるんですか? 37
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • bootstrapFilesに指定したファイルは ”PHPランタイムによって実⾏される”
• 解析を実⾏した環境でも、 実際にrequireできるPathが存在しないといけない • MailDealerは、個⼈の仮想環境にデプロイして動作確認をする 解析を実⾏した開発環境ではrequire先が存在しなかった 38
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • 解決案1 ◦
requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える • 解決案2 ◦ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する 39
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • 解決案1 ◦
requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える • 解決案2 ◦ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する 採用 40
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • 解決案1 ◦
requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える • 解決案2 ◦ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する したんですが…… 41
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • PHPStanは、シンボリックリンクを利⽤するときに 解析がバグるときがある
https://github.com/phpstan/phpstan/issues/7241 • シンボリックを採⽤すべきではないと判断 42
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • 解決案1 ◦
requireのパスを開発環境、デプロイ環境両⽅でも 実⾏できる形式に書き換える • 解決案2 ◦ PHPStanをDocker上で実⾏する Docker上でシンボリックリンクを張り、パスを解決する 採用 43
出ていたエラーの例① Uncaught Error: Failed opening required '〇〇〇.inc' • bootstrapFilesは、”PHPランタイムによって実⾏される” •
幸いにも、requireパスは定数で指定してあった ◦ require LIBPATH . ‘hoge.inc’ • bootstrapFiles内で、定数を上書きして 開発環境のパスでrequireを⾏うようにした ◦ この作業中、定数を使っていないrequireを発⾒しました…… 44
出ていたエラーの例② Call to a member function get() on null •
謎 • 当該オブジェクト作成処理が冗⻑で、 PHPStanの解析上なんらかの問題があった? 45
$object = createObject(); function createObject() { global $object; if (is_null($object))
{ $_object = new SampleObject(); } else { return $object; } return $_object; } 46
出ていたエラーの例② Call to a member function get() on null •
謎 • 当該オブジェクト作成処理がかなり冗⻑で、 PHPStanの解析上なんらかの問題があった? • リファクタリングしたらエラーが出なくなった 47
$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
出ていたエラーの例② Call to a member function get() on null •
謎 • 当該オブジェクト作成処理がかなり冗⻑で、 PHPStanの解析上なんらかの問題があった? • リファクタリングしたらエラーが出なくなった • もしわかる⽅いれば…… 49
ここで学んだこと • bootstrapFilesは、PHPランタイムで実⾏される ◦ bootstrapFilesでは、PHPコードを記述できる • bootstrapFilesは、PHPランタイムで実⾏できる ようにしなければならない 50
おまけ(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";
© RAKUS Co., Ltd. 三度⽬の正直! (とはいいつつ、エラー解消のため何度もトライしてる) 52
© RAKUS Co., Ltd. 無事に解析できた! 53
© RAKUS Co., Ltd. レベル0で 759 errors 54
© RAKUS Co., Ltd. レベル1で 68,374 errors 55
エラー数 • レベル0で759 errors ◦ エラー内容をみても、妥当そう • レベル1で68,374 errors ◦
レベル1から、条件によっては未定義になる変数 をチェックしてくれる ◦ 悲しいかな、これも正しいエラーの可能性が⾼い ◦ これ以上レベルを上げても、爆発的には増えなかった 56
© RAKUS Co., Ltd. さすがに全部対応するのは現実的ではない 57
baseline 既存のエラーを無視し、新しいエラーのみ教えてくれる --generatebaseline というオプションをつけて解析すると baselineファイルを作成してくれる ファイル名の指定なしだと phpstan-baseline.neon が⽣成される ファイル名を指定して .php
ファイルにしたほうが、 解析パフォーマンスが上がるらしい 58
© RAKUS Co., Ltd. baselineを作成して再実⾏! エラーは出ない想定 59
© RAKUS Co., Ltd. がっ、ダメ……! 出る……!エラーが……!まだ……! 60
baselineでも無視できないエラーが存在する Unignorable could not be added to the baseline: Cannot
use [] for reading. 以下のようなコードがあったとき、PHPエラーにはならないが、 PHPStanでは無視できないエラーとして扱われる $arr[] .= ""; Issueでも修正しないと⾔われてしまっているので、 コード修正しかない 61
© RAKUS Co., Ltd. 頑張って修正していきます 62
PHPStanを導⼊してみて 出ているエラーには納得感がある 新規で未定義変数が新たに増えないだけでも品質が上がりそう PHP9では未定義変数が致命的なエラーになる予定なので PHPStanを活⽤すれば合わせて修正が進みそう CI導⼊も進めていく 63
まとめ レガシープロダクトでも、適切に設定することで PHPStanで静的解析が実⾏できる baselineを導⼊し、まずは新規コードから品質を担保する コード品質を諦めない!まずは⼀歩踏み出してみる 次回、運⽤編へ続く…… 64