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.4k
20年もののレガシープロダクトに 0からPHPStanを入れるまで / phpcon2024
hirobe
December 20, 2024
Tweet
Share
More Decks by hirobe
See All by hirobe
PHPでOfficeファイルを取り扱う! PHP Officeライブラリを プロダクトに組み込んだ話
hirobe1999
0
2.5k
PHP8.1で、リソースがオブジェクトに!? マイナーリリースの変更が レガシープロダクトに与えた影響
hirobe1999
0
1.6k
フレームワークが存在しない時代からのレガシープロダクトを、 Laravelに”載せる”実装戦略
hirobe1999
0
1.6k
フレームワークが存在しない時代からのレガシープロダクトを、 Laravelに”載せる”実装戦略
hirobe1999
0
1.9k
新卒PHPer奮闘記 ~配属されたのは3歳違いのプロダクト!?~ / phperkaigi-2022-lt
hirobe1999
0
1.6k
Other Decks in Programming
See All in Programming
CSC305 Lecture 01
javiergs
PRO
1
400
Building, Deploying, and Monitoring Ruby Web Applications with Falcon (Kaigi on Rails 2025)
ioquatix
2
650
Reduxモダナイズ 〜コードのモダン化を通して、将来のライブラリ移行に備える〜
pvcresin
2
690
どの様にAIエージェントと 協業すべきだったのか?
takefumiyoshii
2
620
猫と暮らすネットワークカメラ生活🐈 ~Vision frameworkでペットを愛でよう~ / iOSDC Japan 2025
yutailang0119
0
220
Go言語の特性を活かした公式MCP SDKの設計
hond0413
1
200
Go Conference 2025: Goで体感するMultipath TCP ― Go 1.24 時代の MPTCP Listener を理解する
takehaya
7
1.6k
止められない医療アプリ、そっと Swift 6 へ
medley
1
130
そのpreloadは必要?見過ごされたpreloadが技術的負債として爆発した日
mugitti9
2
3.1k
フロントエンド開発に役立つクライアントプログラム共通のノウハウ / Universal client-side programming best practices for frontend development
nrslib
7
3.9k
overlayPreferenceValue で実現する ピュア SwiftUI な AdMob ネイティブ広告
uhucream
0
170
Domain-centric? Why Hexagonal, Onion, and Clean Architecture Are Answers to the Wrong Question
olivergierke
1
490
Featured
See All Featured
Being A Developer After 40
akosma
91
590k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
140
34k
Optimizing for Happiness
mojombo
379
70k
A Tale of Four Properties
chriscoyier
160
23k
GitHub's CSS Performance
jonrohan
1032
460k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
32
2.2k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
507
140k
How to train your dragon (web standard)
notwaldorf
96
6.3k
Product Roadmaps are Hard
iamctodd
PRO
54
11k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
15
1.7k
Build The Right Thing And Hit Your Dates
maggiecrowley
37
2.9k
For a Future-Friendly Web
brad_frost
180
9.9k
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