Laravel
What’s new in Laravel 8 — part 2
話說分享 part 1 的時候,我自己覺得 Laravel 有一個很大但沒什麼實際用處的變化想要分享,但寫到後來卻忘了,那就是 Laravel 8 也走黑暗模式了,今年以來好像很多網站或系統都開始轉變為黑暗模式,包括被罵慘了的 FB…
我個人覺得 Laravel 8 新版的歡迎頁面還蠻不錯看的,也貼心地列出了這個 Laravel 專案的版本,你們覺得呢?
言歸正傳,繼續來記錄 Laravel 8 的新功能:
- Queueable Anonymous Event Listener
- Time Testing Helpers
- Rate Limiting
Queueable Anonymous Event Listener
在建立事件時,通常會建立一個或多個相對應的 listener 監聽這個事件的觸發,並且進行後續的處理。不過啊,有時候會覺得專案也不大或是處理的事情也不複雜,但要做一個事件卻需要建立事件的類別、Listener 的類別,還要在 EventServiceProvider
中的 $listen
陣列裡為事件註冊好 Listener。雖然說 laravel 有提供了 event:generate
的指令來幫忙,但如果你想偷懶的話,其實可以在 EventServiceProvider
的 boot
裡,用 closure
的方式為事件註冊 Listener,例如:
use App\Events\OrderShipped;public function boot()
{
Event::listen(function (OrderShipped $event) {
dump("我會處理 OrderShipped 事件");
});
}
這個做法在 Laravel 7 以前就支援了,但會遭遇一個問題:在 Laravel 中,如果想要非同步處理事件,也就是想先把 Listener 放在 Queue 裡,之後再讀出來處理,必須在 Listener 類別中實作 ShouldQueue
介面,詳請可參考 Queued Event Listeners。問題就在這裡,如果我們是如上所示地用 closure
來處理事件,那要怎麼做到可以 Q 起來呢?
在 Laravel 7 之前,恐怕只能重新做一個 Listener 類別出來了。而在 Laravel 8 中則 queueable
函式來解決這個問題,其用法如下:
use function Illuminate\Events\queueable;public function boot()
{
Event::listen(queueable(function (OrderShipped $event) {
dump("我會處理 OrderShipped 事件");
}));
}
你看看你看看,只需要 use queueable
這個函式,然後將 closure
傳入這個 queueable
函式即可,是不是很簡單呢?
如果你過去有用過 Queued Listener,可能會知道在 Listener 中是可以自行設定$connection
、 $queue
、$delay
等屬性的。別擔心,Laravel 沒有忘記這些設定,甚至他還提供了 catch
,讓你可以捕捉這個匿名的 Listener 裡的錯誤:
use function Illuminate\Events\queueable;
use Throwable;public function boot()
{
Event::listen(queueable(function (OrderShipped $event) {
dump("我會處理 OrderShipped 事件");
Log::info('故意錯誤');
})
->onConnection('redis')
->onQueue('podcasts')
->delay(now()->addSeconds(10))
->catch(function (OrderShipped $event, Throwable $e) {
\Log::info('捕捉到例外');
}));
}
Time Testing Helpers
如果要請朋友們一起來分享過去在寫測試的時候有沒有遇過什麼棘手的問題,我相信「時間」應該蠻容易被提起的,例如,當你的文章有設定發佈時間時,要怎麼測試到了這個發布時間,文章是否真的能被發布出去呢?程式碼中的實作可能會拿 published_at
來跟現在時間比較,那測試的時候會希望「現在」時間早於這個 published_at
,而又可以讓「現在」時間晚於這個 published_at
,這樣兩種案例就都能測試到。
為了解決這個問題,Laravel 8 中提供了「時光旅行」來解決這個問題:
// 旅行到五天後
$this->travel(5)->days();// 旅行到特定時間
$this->travelTo(now()->subHours(6));// 旅行回來
$this->travelBack();
大家可以視自己的場景來應用,比較有趣的是他的實作在 Illuminate/Foundation/Testing/Concerns/InteractsWithTime.php 跟 Illuminate/Foundation/Testing/Wormhole.php,發現了嗎? Wormhole
蟲洞,貼切到不知道該怎麼說了…
挑其中一個實作來看看就可以發現,其實就是叫用了 Carbon
的 setTestNow
:
/**
* Travel forward the given number of milliseconds.
*
* @param callable|null $callback
* @return mixed
*/
public function milliseconds($callback = null)
{
Carbon::setTestNow(Carbon::now()->addMilliseconds($this->value));return $this->handleCallback($callback);
}
題外話,根據文件記載,這是 Taylor 從 RoR 得到的靈感,所以其實也不用鄙視來鄙視去,好好的互相學習優點,不是很不錯嗎?
Rate Limiting
在 Laravel 7 以前,我們可以利用 throttle
這個 middleware 來達成限制 API的請求頻率限制,例如在 Kernel.php
這樣設定,就可以限制每一分鐘只能請求 60 次:
'api' => [
'throttle:60,1',
]
根據 Laravel 7 的文件,其設定的彈性最多就是這樣 throttle:10|rate_limit,1
,也就是尚未登入的使用者每一分鐘可請求 10 次,已經登入過的使用者則根據其 User model 中的 rate_limit
屬性來限制。
在討論 Laravel 8 之前,我們先來看看 Rate Limiting 在 Laravel 中是怎麼實作的。首先,從 kernel.php
中我們可以看到 throttle
其實是 \Illuminate\Routing\Middleware\ThrottleRequests
這個 middleware,於是打開 ThrottleRequests
來看看,會發現這個 middleware 會在建構式中設定一個 RateLimiter
,根據說明其實作為 \Illuminate\Cache\RateLimiter
。簡單看了一下程式碼可以理解 ThrottleRequests
主要是在處理 request 跟 rate limiting 的參數解析,而真正判斷是否有超過頻率限制的實作是在 RateLimiter
中, RateLimiter
則是利用 cache 來做紀錄。
在我的 Laravel 7 的測試專案中,因為是用預設值,所以是 cache 在 file 中,在我執行了某個 API 兩次後,可以在 /storage/framework/cache/data 中找到一個檔案,其內容為如下,如果把 1601013658 拿去轉換一下就會發現它是一個 timestamp,其值為 2020年9月25日星期五 14:00:58,恰好是我進行測試的時間,而第二個數字 2 則是我執行 API 的次數,每當我多執行一次,這個數字就會往上遞增,直到我所設定的限制為止。
此外,如果檢視一下呼叫 API,其回覆的 header,也可以看到有以下兩個 header 出現在 response 中:
如果已達限制,header 就會變成這樣,多了 retry-after
及 x-ratelimit-reset
來讓呼叫方知道多久後可以重試。
以上簡單地看了一下 Laravel 7 之前對 Rate Limiting 的實現。現在來看看 Laravel 8 多了什麼新東西,基本上 throttle
還是在的,用法也可以沿用。但在 Laravel 8 中,如果打開 app/Providers/RouteServiceProvider
,可以看到 boot
中多執行了 configureRateLimiting
,這個函式預設會執行:
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(3);
});
}
這裡的 api
可以替換成任何名稱,它對應的地方是在 kernel
中 throttle:api
的 api,也就是當你寫 RateLimiter::for('azole', ...
時,如果要把這個設定用上,那在 kernel
中就要寫成 throttle:azole
這樣。
不過,你們有發現似曾相似的地方呢?其實我們在上面解析 Laravel 7 時就有看過RateLimiter
,在 RouteServiceProvider
往上找一下可以看到Illuminate\Support\Facades\RateLimiter
,打開這個 Facade,果然就還是 Laravel 7 中的 Illuminate\Cache\RateLimiter
。也就是說其實跟 Laravel 7 沒有太多的差別,但在 Laravel 8 中把 RateLimiter
放出來讓我們可以自行設定,這樣做有什麼好處呢?
來看看官網給的範例:
RateLimiter::for('uploads', function (Request $request) {
return $request->user()->vipCustomer()
? Limit::none()
: Limit::perMinute(100);
});
還記得嗎?我們在 Laravel 7 之前頂多設定一下已登入、未登入使用者的限制數量,但 Laravel 8 改成這樣之後,就可以針對該次的 request 做客製化的限制,以上述範例來說,可以針對已登入的使用者,判斷他是不是 VIP 客戶而有不同的限制,是不是很方便呢?
官網文件中還有另外一個範例:
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5),
Limit::perMinute(3)->by($request->input('email')),
];
});
它可以回傳一個 Limit 陣列,然後陣列中的每一個 limit 都會依序被評估。
在簡單地看過 Laravel 7 跟 Laravel 8 Rate Limiting 實作的部分後,真心推薦不管是不是要用或升級到 Laravel 8,都可以比較看看這兩個版本的實作方式、比較一下做了什麼修改。我自己看來,其實調整的幅度並不是很大,卻能讓整體的使用彈性大幅地上升,覺得還蠻驚豔的,我想這也是學習怎麼開發程式的一個很棒的方式。