前言
在之前使用 PHPStan 對代碼進行靜態檢查的時候,如果把檢查等級提升到 9,在把一個 mixed 類型的值傳遞給需要明確類型的參數時,就會出現提示。
function foo(int $a): int
{
return $a * 1;
}
function bar(): mixed
{
return 'a';
}
$a = bar();
$b = foo($a); // Parameter #1 $a of function foo expects int, mixed given.
var_dump($b);
這是因為我們這裏 bar 返回了一個 mixed 類型,而 foo 期待一個 int 類型的參數。
這時候你可能覺得很簡單,使用 (int) 將 bar 的返回值強制轉換一下不就好了嗎?
$a = (int) bar(); // Cannot cast mixed to string.
$b = foo($a);
var_dump($b);
而這樣同樣會觸發另一個問題,因為 mixed 可以是任意類型,它是不可靠的,這樣強制轉換的話,有可能出現異常。
要解決這個問題,我們有多種方案:
// 1、使用 is_int 進行檢查,確保 `$a` 的一定是 int 類型的時候,才進行下面的操作。
$a = bar();
if (is_int($a)) {
$b = foo($a);
var_dump($b);
}
// 2、使用註釋,人為地告訴 PHPStan,`$a` 是一個 int
/** @var int $a */
$a = bar();
$b = foo($a);
var_dump($b);
// 3、使用 assert(內置或者第三方的,其實跟第一種差不多,是在失敗時會拋出異常),此處以 webmozart/assert 舉例
use Webmozart\Assert\Assert;
// ...
$a = bar();
Assert::integer($a);
// 注意,如果我們啓用了 PHP 的嚴格模式的話,我們實際還需要在這裏對 $a 執行強制轉換才行。
$b = foo($a);
var_dump($b);
其中第一種和第三種明顯最安全,但是要引入額外的判斷,適合一些確實來源類型不確定的場景。
第一種沒有添加 else 的處理,而第三種會在檢查到不是 int 時拋出異常,這些都需要額外處理。
而第二種則是一種欺騙,但在一些場景中,我們遇到的更多其實是這樣的情況。
而在 Laravel 中,我們最常遇到的就是從請求獲取參數,大部分時候,我們都在使用 Request 的 input、get 方法,而這兩個方法,返回的值就是 mixed 類型的,而實際上我們可能已經在表單驗證中,這個字段只能是字符、數字、布爾。如果我們這裏為了糊弄 PHPStan 的話,就得編寫類似於上面的代碼。
認識 Fluent
那有沒有更好的方案?答案是有的,那就是 \Illuminate\Support\Fluent(下文簡稱 Fluent 或 Fluent)。
Fluent 這個類在 Laravel 中存在了很久,早期它主要承擔了一些簡單的 get 和類數組訪問的操作。
而在 Laravel 中,因為這個 PR\# 53665,開始變得與眾不同。
根據 PR 的介紹,下面這些方法,從 \Illuminate\Http\Concerns\InteractsWithInput 移動到了 \Illuminate\Support\Traits\InteractsWithData,Fluent 使用 InteractsWithData 這個 Trait。
has($key)
only($keys)
exists($key)
filled($key)
hasAny($keys)
missing($key)
except($keys)
anyFilled($keys)
isNotFilled($key)
collect($key = null)
enum($key, $enumClass)
enums($key, $enumClass)
str($key, $default = null)
integer($key, $default = 0)
float($key, $default = 0.0)
string($key, $default = null)
boolean($key = null, $default = false)
date($key, $format = null, $tz = null)
whenHas($key, callable $callback, ?callable $default = null)
whenFilled($key, callable $callback, ?callable $default = null)
whenMissing($key, callable $callback, ?callable $default = null)
注意其中的 enum/enums/str/int/float/string/boolean/date/collect/array 方法,這些方法其實是自 Laravel 9 開始被陸陸續續添加到 InteractsWithInput 中。這些方法會將接收到的參數經過轉換,然後返回預定的類型,我們便可以在 Request 上使用 integer/bool 等方法來接收請求參數。
因為 PHPStan 並不檢查 vendor 裏面的實現,所以,當這些方法的簽名(或者存根文件)中聲明瞭將會返回預定的類型,那麼 PHPStan 就會選擇信任。至此,這些錯誤將消失,同時我們獲得了類型安全的數據。注意,這裏框架內部並沒有欺騙 PHPStan,而是確確實實地對數據進行了轉換,只是在某些情況下,這些轉換可能不符合我們的預期。
同時,也意味着我們在使用時也要注意,因為大多數方法,它其實就是使用的強制類型轉換,比如在一些為 null 的字段,可能接收到的就不符合預期了。這時候可以使用 blank 或者 filled 方法來檢查。
至此,這篇文章已經接近了尾聲,我們現在知道 Fluent 類使用了 InteractsWithData 所以支持這裏面所有的方法,以及未來可能的方法。
而在 Laravel 11 之後,在其內部也有許多應用:
- 比如
Http包的響應中,添加了fluent方法,將響應轉換成Fluent,你現在可以使用Fluent上的所有方法,比如,它支持嵌套的對象屬性獲取,就像這樣$response()->fluent()->int('product.id')。 Request::safe()方法的返回值就是經過Fluent包裝的Request::validated(),通過表單驗證的數據。- 還提供了
fluent助手函數,可以方便地把對象/數組包裝成Fluent對象。 - 你也可以把
Fluent應用到你的項目中去。 - 在上週剛剛發佈的 Laravel 12.19.0 版本中,還為模型添加了
AsFluent這個 Cast,用來方便地把模型上的json字段,在讀取時轉換為Fluent對象。
除此之外,你可能還意外發現,Fluent 還有一些奇怪的應用:
比如在遷移中,實際上大部分類型方法比如(string/integer/mediumText)等,返回的都是一個派生自 Fluent 的對象,因為這裏用 Fluent 的另一個特性,它在內部擴展了 __call 方法,當你在上面調用一些不存在的方法的時候,它實際上會把方法名作為 key、參數作為值保存到實例中,然後繼續返回自身,這樣用來保存一些字段的配置項,在最終生成遷移語句的時候讀取這上面的配置即可。
再比如 \Illuminate\Foundation\Bus\Dispatchable 這個 Trait,我們一般會在 Event 中使用,它包含了兩個比較有用的方法,dispatchUnless 和 dispatchIf,dispatchUnless 的第一個參數如果評估為 false,則投遞這個 Event,dispatchIf 則相反。
public static function dispatchIf($boolean, ...$arguments)
{
if ($boolean instanceof Closure) {
$dispatchable = new static(...$arguments);
return value($boolean, $dispatchable)
? static::newPendingDispatch($dispatchable)
: new Fluent;
}
return value($boolean)
? static::newPendingDispatch(new static(...$arguments))
: new Fluent;
}
而 dispatch 實際返回的其實是 \Illuminate\Foundation\Bus\PendingDispatch 這個對象,在這上面我們可以調用 onConnection/onQueue/delay/afterCommit 等等方法,而在評估為不派發時,這裏實際就會返回一個 Fluent 對象,也是利用了前面提到的這個特性,讓我們在評估為失敗時調用這些方法也不至於報錯(方法不存在)。
其他應用
除此之外,Laravel 中還有一種類似於 Fluent 的應用,那便是 Config。config 作為我們經常使用的方法,我們經常使用 config('app.url') 或者類似的方式來獲取配置,但是因為 config 函數的特殊性,在它沒有接收參數或者第一個參數為 null 的時候,他其實會返回一個 Repository 對象(原文字面意思理解為Config對象,但實際上返回的是Illuminate\Config\Repository實例),而在傳入數組時,則又會返回 void。雖然對於 PHPStan 你可以標註完整的返回類型,但是它還是無法猜測你獲取到的配置項的值。
而 Config 現在也為我們提供瞭如下方法:string/integer/float/boolean/array。現在,你可以使用這些方法獲得預定類型的配置項值,但是,它不是一個 Fluent,因為它會檢查獲取出來的配置項值,如果不滿足對應的類型,它就會拋出異常,如果滿足,就會轉為指定的類型,就像我們前面寫的第三種方式類似,而 Fluent 它是會進行轉換的,而不檢查。
結語
最後,值得一提的是,如果你使用 PHPStan 進行類型檢查,那麼你其實也不必一味追求更好的檢查等級,選擇合適的即可(比如 level 8,就不會檢查上述提到的 mixed)。
雖然更嚴格的檢查會驅使你寫出更嚴謹的代碼,但是往往也要耗費更多的精力,但是它可以在未來幫你節約不少時間。
本文使用 Gemini 進行了排版優化、專有名詞處理、錯別字、錯誤用詞處理。
本作品採用 知識共享署名 4.0 國際許可協議 進行許可。