PHPStan:無(wú)需寫(xiě)測(cè)試就能找到代碼中的 Bug
每當(dāng)我看到開(kāi)發(fā)人員從 Java 或 C# 等編譯語(yǔ)言切換到 PHP 這樣的解釋語(yǔ)言時(shí)解放了生產(chǎn)力后感到很高興荒叶。除了這些常規(guī)的執(zhí)行模型(發(fā)起、處理請(qǐng)求和結(jié)束請(qǐng)求)和更短的反饋環(huán)(無(wú)需等待編譯器)外输虱,還有一個(gè)能解決開(kāi)發(fā)人員日常問(wèn)題的開(kāi)源框架生態(tài)系統(tǒng)些楣,因此,PHP 是目前來(lái)說(shuō) web 開(kāi)發(fā)中最流行的語(yǔ)言宪睹。
但它有一個(gè)缺點(diǎn)愁茁。
你會(huì)在什么時(shí)候發(fā)現(xiàn)錯(cuò)誤?
編譯型語(yǔ)言需要在程序運(yùn)行之前了解每個(gè)變量的類(lèi)型,每個(gè)方法的返回類(lèi)型亭病。這就是為什么編譯器需要確保程序是沒(méi)有錯(cuò)誤的鹅很,并且會(huì)在源碼中向你指出這些類(lèi)型的錯(cuò)誤,比如調(diào)用了未定義的方法或者是向某個(gè)函數(shù)傳遞了錯(cuò)誤數(shù)量的參數(shù)罪帖。在把應(yīng)用程序部署到生產(chǎn)環(huán)境前促煮,編譯器算是第一道防線。
然而 PHP 就不會(huì)這樣了整袁。如果程序出錯(cuò)菠齿,會(huì)執(zhí)行到錯(cuò)誤的代碼的時(shí)候崩潰。在測(cè)試 PHP 應(yīng)用時(shí)葬项,不管是自動(dòng)化測(cè)試還是手動(dòng)測(cè)試泞当,開(kāi)發(fā)人員都會(huì)花費(fèi)大量時(shí)間去查一些其它編譯型語(yǔ)言不會(huì)犯的錯(cuò)從而減少測(cè)試實(shí)際業(yè)務(wù)邏輯的時(shí)間。
我想改變這一點(diǎn)。
歡迎來(lái)到 PHPStan 的世界
現(xiàn)階段 PHP 實(shí)踐所產(chǎn)生的代碼庫(kù)中襟士,我們可以確定大部分?jǐn)?shù)據(jù)的類(lèi)型盗飒,并且轉(zhuǎn)換為靜態(tài)類(lèi)型的語(yǔ)言,盡管還保留著一些動(dòng)態(tài)語(yǔ)言的特性陋桂。人們把現(xiàn)在的 PHP 代碼庫(kù)變得跟其他語(yǔ)言一樣更加有趣逆趣。面向?qū)ο螅蕾?lài)注入以及設(shè)計(jì)模式的使用已經(jīng)變得非常普遍嗜历。
這讓我想到了 PHP 的 靜態(tài)分析工具宣渗,它將替代其他語(yǔ)言的編譯器角色。我花了很多時(shí)間研究它梨州,并且已經(jīng)使用它的各種開(kāi)發(fā)版本來(lái)檢查我們的代碼庫(kù)超過(guò)一年痕囱。
它就是 PHPStan, 開(kāi)源且免費(fèi)
它目前校驗(yàn)什么?
- 有關(guān)類(lèi)中涉及的暴匠,對(duì)象實(shí)例鞍恢, 錯(cuò)誤/異常捕獲,類(lèi)型約束以及其他語(yǔ)言結(jié)構(gòu)的存在性每窖。 PHP 照舊不會(huì)檢查這些帮掉, 但是會(huì)展現(xiàn)其中未被使用的代碼。
- 被調(diào)用的方法和函數(shù)的存在性和可訪問(wèn)性窒典。同樣也會(huì)檢查他們的參數(shù)個(gè)數(shù)蟆炊。
- 方法是否返回了它聲明的返回值類(lèi)型。
- 被訪問(wèn)成員變量的存在性和可見(jiàn)性瀑志。它也可指出是否將一個(gè)其他的類(lèi)型的值賦給了既定類(lèi)型的成員變量涩搓。
- sprintf/printf 函數(shù)基于格式化字符串所應(yīng)接收的參數(shù)個(gè)數(shù)。
- 分支和循環(huán)范圍中的變量的存在性后室。
- 無(wú)用的形式指定缩膝。例如 (string) 'foo' ,以及不同類(lèi)型變量間的嚴(yán)格比較 (=== 和 !==)岸霹,因?yàn)樗麄兊慕Y(jié)果總為 false。
這個(gè)清單的內(nèi)容隨著每次發(fā)布都在遞增将饺。但成就 PHPStan 也不會(huì)只仰賴(lài)此一技之微贡避。
PHPStan 迅疾如飛...
它設(shè)法一次性檢查整個(gè)代碼庫(kù)。 它無(wú)需多次遍歷代碼予弧。 只需瀏覽您想要分析的代碼刮吧,例如 你寫(xiě)的代碼。它無(wú)需解析和分析第三方依賴(lài)項(xiàng)掖蛤。 相反杀捻,它使用反射來(lái)找出有關(guān)你代碼庫(kù)中引用的他人代碼的有用信息。
PHPStan 能在一分鐘里檢查我們的代碼庫(kù) (6000 個(gè)文件蚓庭, 600k LOCs) 致讥。它可在一秒內(nèi)完成自查仅仆。
...可擴(kuò)展性
即便當(dāng)前正在使用靜態(tài)類(lèi)型,開(kāi)發(fā)者也可以合法的使用 PHP 的動(dòng)態(tài)語(yǔ)法特性垢袱,例如 **get, **set 和 __call 這些魔術(shù)方法墓拜。它們可以在運(yùn)行時(shí)去定義新屬性和方法。通常请契,靜態(tài)分析都會(huì)爆出屬性和方法未定義咳榜,但是有一種機(jī)制可以告訴引擎如何創(chuàng)建新的屬性和方法。
它得益于對(duì)允許用戶擴(kuò)展的原生 PHP 反射的自定義抽象爽锥。更多細(xì)節(jié)可查看 README 中類(lèi)反射擴(kuò)展章節(jié)涌韩。
某些方法返回的類(lèi)型取決于它的參數(shù)。它可以取決于你傳遞給它的類(lèi)名氯夷,也可能返回與傳遞的對(duì)象相同的類(lèi)的對(duì)象臣樱。這就是 動(dòng)態(tài)返回類(lèi)型擴(kuò)展 的用途。
壓軸語(yǔ): 如果你想自己出一個(gè) PHPStan 的新的檢查項(xiàng)肠槽, 你可以自力更生擎淤。可以提出基于特定框架的規(guī)則秸仙,例如檢查 DQL查詢(xún)中引用的實(shí)體和字段是否存在嘴拢,或者你選擇的 MVC 框架中生成的鏈接是否和現(xiàn)存的控制器有關(guān)。
選擇規(guī)范級(jí)別
我使用過(guò)其他工具寂纪,并將之集成進(jìn)現(xiàn)有的代碼庫(kù)中席吴,這種體驗(yàn)真是往事不堪回首。他們爆出成千上萬(wàn)的錯(cuò)誤讓你沒(méi)法使用捞蛋。
取而代之孝冒,我回顧如何集成 PHPStan 到剛進(jìn)入開(kāi)發(fā)階段的代碼庫(kù)中。 首個(gè)版本的功能不是很強(qiáng)大拟杉,這時(shí)并未發(fā)現(xiàn)多少錯(cuò)誤庄涡。但從集成的角度來(lái)看,它還是非常不錯(cuò)的?---?有空時(shí)搬设,我就為它增加新規(guī)則穴店,我修復(fù)了它在版本庫(kù)中找到的錯(cuò)誤,并將新代碼合并到主分支拿穴。我們會(huì)使用新版本幾周用來(lái)發(fā)現(xiàn)其找到的錯(cuò)誤泣洞,并不斷重復(fù)這件事。這種逐級(jí)增加的規(guī)范性的做法在實(shí)踐中看來(lái)大有裨益默色,所以我使用 PHPStan 的現(xiàn)有功能來(lái)模擬它球凰。
默認(rèn)情況下,PHPStan 只檢查它確定的代碼---常量,實(shí)例化呕诉,調(diào)用$ this的方法缘厢,靜態(tài)調(diào)用的方法,函數(shù)和各種語(yǔ)言結(jié)構(gòu)中的現(xiàn)有類(lèi)义钉。 通過(guò)增加級(jí)別(從默認(rèn)值0到當(dāng)前值4)昧绣,您還可以增加它對(duì)代碼所做的假設(shè)數(shù)量以及它檢查的規(guī)則數(shù)量。
如果內(nèi)建級(jí)別無(wú)法滿足你的要求捶闸,你同樣也可以自定義規(guī)則夜畴。
少寫(xiě)單元測(cè)試! (披沙揀金)
可能這個(gè)建議你聞所未聞。即便是非常細(xì)碎的代碼删壮,開(kāi)發(fā)者也不得不編寫(xiě)單元測(cè)試贪绘,因?yàn)檫@方面犯錯(cuò)的幾率都是均等的,例如簡(jiǎn)單的拼寫(xiě)錯(cuò)誤或者忘記將結(jié)果賦值給變量央碟。為那些經(jīng)常出現(xiàn)在控制器或者門(mén)臉中的轉(zhuǎn)發(fā)代碼編寫(xiě)單元測(cè)試是很不劃算的事税灌。
單元測(cè)試也有其成本。它們同樣也是代碼亿虽,難逃編寫(xiě)和維護(hù)的窠臼菱涤。最理想的做法就是在持續(xù)集成服務(wù)器上,每次更改時(shí)都運(yùn)行 PHPStan洛勉,從而在無(wú)需單元測(cè)試的情況下防止此類(lèi)錯(cuò)誤的產(chǎn)生粘秆。實(shí)現(xiàn)100%的代碼覆蓋率真的很難,并且非常昂貴收毫,但你可以靜態(tài)分析100%的代碼攻走。
至于單元測(cè)試的重點(diǎn)應(yīng)當(dāng)集中在靜態(tài)分析代碼難以察覺(jué)的,容易出錯(cuò)的地方此再。包括:復(fù)雜的數(shù)據(jù)過(guò)濾昔搂,循環(huán),條件判斷输拇,乘除法包含舍入的計(jì)算等摘符。
站在巨人的肩膀上
如果不是 Nikita Popov 創(chuàng)建了 PHP Parser。就不會(huì)有 PHPStan 的出現(xiàn)策吠。
PHP 在 2016 年開(kāi)始廣泛使用 包管理议慰, 單元測(cè)試 和 編碼標(biāo)準(zhǔn) 的工具。然而到現(xiàn)在也沒(méi)有一個(gè)廣泛使用的工具奴曙,可以在不運(yùn)行代碼的情況下檢查代碼中的錯(cuò)誤。所以我創(chuàng)建了一個(gè)易于使用草讶,快速洽糟,可擴(kuò)展的版本,既不會(huì)對(duì)您的代碼有嚴(yán)格的要求,你還會(huì)從這些檢查中受益坤溃。查看 GitHub 倉(cāng)庫(kù) 拍霜,了解如何將其集成到您的項(xiàng)目中!