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
1.1k
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.9k
PHP8.1で、リソースがオブジェクトに!? マイナーリリースの変更が レガシープロダクトに与えた影響
hirobe1999
0
1.3k
フレームワークが存在しない時代からのレガシープロダクトを、 Laravelに”載せる”実装戦略
hirobe1999
0
1.4k
フレームワークが存在しない時代からのレガシープロダクトを、 Laravelに”載せる”実装戦略
hirobe1999
0
1.6k
新卒PHPer奮闘記 ~配属されたのは3歳違いのプロダクト!?~ / phperkaigi-2022-lt
hirobe1999
0
1.4k
Other Decks in Programming
See All in Programming
ErdMap: Thinking about a map for Rails applications
makicamel
1
1.3k
Simple組み合わせ村から大都会Railsにやってきた俺は / Coming to Rails from the Simple
moznion
3
4k
Terraform で作る Amazon ECS の CI/CD パイプライン
hiyanger
0
120
ASP. NET CoreにおけるWebAPIの最新情報
tomokusaba
0
300
Pythonでもちょっとリッチな見た目のアプリを設計してみる
ueponx
1
300
SRE、開発、QAが協業して挑んだリリースプロセス改革@SRE Kaigi 2025
nealle
3
3.6k
AWS Lambda functions with C# 用の Dev Container Template を作ってみた件
mappie_kochi
0
230
DROBEの生成AI活用事例 with AWS
ippey
0
110
テストをしないQAエンジニアは何をしているか?
nealle
0
110
Compose でデザインと実装の差異を減らすための取り組み
oidy
1
280
Writing documentation can be fun with plugin system
okuramasafumi
0
110
ATDDで素早く安定した デリバリを実現しよう!
tonnsama
1
2.5k
Featured
See All Featured
Large-scale JavaScript Application Architecture
addyosmani
510
110k
Stop Working from a Prison Cell
hatefulcrawdad
267
20k
Navigating Team Friction
lara
183
15k
Documentation Writing (for coders)
carmenintech
67
4.6k
Music & Morning Musume
bryan
46
6.3k
The World Runs on Bad Software
bkeepers
PRO
67
11k
GitHub's CSS Performance
jonrohan
1030
460k
A Modern Web Designer's Workflow
chriscoyier
693
190k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
45
2.3k
Fashionably flexible responsive web design (full day workshop)
malarkey
406
66k
Build your cross-platform service in a week with App Engine
jlugia
229
18k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
3k
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