Notion 初體驗
使用過許多筆記類的軟體,包括 Evernote、Bear、Google Keep 等,但總是會覺得某些地方不足。
使用過許多筆記類的軟體,包括 Evernote、Bear、Google Keep 等,但總是會覺得某些地方不足。
TestResponse 是一個輔助測試 response 用的物件,它內建混入(mixin)了 Response 物件功能:
今天要來分析內建測試是如何實作的,先來看官方測試範例程式碼。
public function testBasicTest() |
它對應會跑的 Route 如下:
Route::get('/', function () { |
Routing 的分析可以再回頭看 Day 12。
這幾會來看 Laravel 是如何從 get()
去取得 response,並得知 statue code 或其他結果。
Laravel Framework 有一個抽象的 TestCase 類別,首先要了解測試一開始初始化了哪些東西,這可以從 setUp()
得知:
protected function setUp() |
Application 是 Laravel 的核心,所以一開始得先初始化。
protected function refreshApplication() |
分析 bootstrap 流程曾提過,bootstrap/app.php
的任務是提供一個可以用在任何場景的 Application,包括測試。而 bootstrap()
方法是為了載入必要的設定檔等,這樣測試程式碼才能正常使用 Config 等元件。
接著,如果 TestCase 有標記特定的 trait,如 DatabaseTransactions,就會有特定的行為,這個 magic 是由 setUpTraits()
所實作的。後面則是清除設定,讓測試可以從乾淨的狀態從頭執行。
get()
與其他 HTTP 相關的方法是寫在 MakesHttpRequests 裡:
public function get($uri, array $headers = []) |
其他相關的 HTTP method 與 get()
一樣,最終都會呼叫 call()
:
public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null) |
仔細觀察可以發現,它跟 index.php 有很多相同的呼叫方法,而 index.php 有多呼叫 Response send 方法。
最後的 response 測試輔助物件則提供了 assertStatus()
等方法,來驗證最後的 response。
繼續看 RoutesRequests 下面的方法。
createDispatcher()
sendExceptionToHandler()
prepareResponse()
Lumen 並沒有使用 Laravel 的 Routing,而是使用自定義的 Router,把 Router 和 FastRoute 結合的就是這個方法:
protected function createDispatcher() |
simpleDispatcher 是 FastRoute 提供 Dispatcher 的工廠方法。傳入的 $r
會是 FastRoute 專用的 RouteCollection。可以看到它使用 addRoute()
把 Lumen Router 存放的 Route 再轉存到 FastRoute Collection 裡。
最後 dispatcher 屬性,將會是 FastRoute 的 Dispatcher。
protected function sendExceptionToHandler($e) |
Laravel 的 ErrorHandler 分析可以參考 Day31 分析自定義錯誤頁
Laravel 也有 prepareResponse(),Lumen 就像是精簡版一樣:
public function prepareResponse($response) |
以上就是 dispatch()
詳細分析的過程,接著明天再來看 Router 的實作。
Lumen 在處理跟 Request 相關的程式,都放在 RoutesRequests 這個 trait 裡,包括今天要看的 dispatch()
。
與 Laravel 類似地,Lumen 也是被拆成 Lumen 與 Lumen Framework 兩部分。而 Lumen Framework 則是使用第一天提到的 Illuminate 套件實作出來的。
最近幾天會來研究一下 Lumen 怎麼使用 Laravel 抽離出來的套件,會使用 v5.7.6 版,從 src 目錄可以知道 Lumen Framework 客製化的套件庫樣貌:
Auth |
Concerns
與Providers
不大像是客製化的一部分,而比較像 helper
了解 Lumen 的做法後,就能知道如何使用 Laravel 提供的 Contract 與輪子,組合出一個自幹框架。
事實上,重頭戲還是會在 Routing,因為 Lumen 使用了 nikic/fast-route
作為解析路由器。如何取代原有 Routing 正是 Lumen 程式碼最值得看的地方。
今天只起個頭,休息一下,明天再繼續努力了。
tap()
之前有提過,是 helpers.php 的方法之一。
function tap($value, $callback = null) |
先不管 HigherOrderTapProxy,來看剩下的原始碼:
function tap($value, $callback) |
它需要傳入一個 $value,然後它會再回傳出來,因此可以知道下面這個寫法是可行的:
tap(new Collection(), $callback)->each->pay(); |
再來因為它中間有做 $callback($value)
,因此上面這個方法的全貌可能會是長這樣的:
$callback = function($collection) { |
反過來看,如果沒有 tap() 函式的話,我們可能需要這樣寫:
$collection = new Collection(); |
咦,看起來似乎不用 tap() 寫起來比較乾淨。不是這樣的,是 Collection 並不適合用在這個地方。
最常看到的就是在物件初始化的時候,比方說分析 Session 提到了下面這段程式碼:
protected function startSession(Request $request) |
使用 tap()
的好處之一是剛剛有提到的,這對要使用串聯方法是有利的;另一個好處則是:有時候我們會希望對某個實例做某些事,而做這些事會需要產生一些專用的暫時變數,這時 tap() 因為可以使用 Closure,所以可以把這些暫存變數「關」在裡面,外面就不會被這些暫時變數干擾到。
雖然是這樣,但每次要寫一堆 callable 就很煩,因此出現了另一個選擇:HigherOrderTapProxy,來看看它的原始碼:
// 前面只是建構的時候把 value 存到 target 而已,所以省略 |
這很像 proxy pattern,唯一不同的地方在於,它固定會回傳 self。以 分析 Log 的例子來說,原本程式碼與改寫後的程式碼如下:
tap($this->createEmergencyLogger(), function ($logger) use ($e) { |
跟 Higher Order Messages 很像,可以省略掉一層 callback,但同時也有一樣的使用條件:以 callback 的寫法,只允許一行程式碼。
Higher Order Messages 是一個像魔法一般的功能。
來看看官方提供的範例:
$invoices->each(function($invoice) { |
原本要寫一堆 callback,現在只要使用一連串屬性的取法,就能得到一樣的結果。
以上例第一個例子為例:
$invoices->each->pay(); |
可以猜想得到,each 會是一個委任出去的角色,這從原始碼最上面註解的屬性可以找得到:
/** |
通常 Laravel 或大部分開源的原始碼,會寫在最上面的屬性和方法,通常都是 magic method 實作出來的。本例是屬性,所以會是由 __get()
實作:
protected static $proxies = [ |
這裡直接建構出 HigherOrderCollectionProxy 出來用,而帶入的 $key
以上例來說,會是 each
。這個元件的實作超級簡單,除了建構子單純是保存狀態之外,就只有實作兩個 magic method:
註:下面原始碼的
$this->method
,即為上面建構帶入的的$key
。
public function __get($key) |
由原始碼可以看出,原本要寫的 callback,將會由委任的物件建立。同時也可以得知,要成為 Higher Order Messages 的一員,參數必須要有 callback。
而哪時會使用 __get()
,哪時會使用 __call()
,下面是一個比較容易理解的比較:
// __get() |
接著來看 __get()
實際在做的事,裡面的 $key
指的是上例的 name
。因此原始碼與等價的程式碼比對如下:
return $this->collection->{$this->method}(function ($value) use ($key) { |
同理,__call()
的 $key
是 pay
。原始碼與等價的程式碼比對如下:
return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { |
從上述等價原始碼可以了解,使用 Higher Order Messages 的時機會是,當方法是在 proxy 的列表裡,且 callback 裡面單純只回傳一個特定值、或呼叫一個特定方法時,就能考慮使用它。反之,需要兩行以上的程式碼時,就無法使用這個功能了。
Higher Order Messages 是個很有趣的實作,短短不到 100 行程式碼,就能讓使用它的開發者少寫非常多程式碼,而且可讀性也不差,真的是非常厲害。
先大概列幾個與原生 PHP 函式相關的方法。
Native | Collection |
---|---|
array_chunk() | chunk() |
array_combine() | combine() |
array_diff() | diff() |
array_flip() | flip() |
array_filter() | filter() |
array_map() | map() |
array_reduce() | reduce() |
array_slice() | slice() |
array_walk() | each() |
通常 Collection 的實作,都有加強原生對 array 處理的方便性,這些就不多做說明,來提一些比較有趣的。
transform()
這個方法跟 map()
其實是一樣的,但它會改變原本的物件的內容:
public function transform(callable $callback) |
pipe()
這個 pipe 的意義,與 Bash 的 |
類似,把該物件交由 callback 處理
public function pipe(callable $callback) |
tap()
public function tap(callable $callback) |
之前曾提過 tap()
函式。這個方法也是類似的概念,它的等價程式碼如下:
$callback = function($items) { |
讓 collection 自帶 tap()
方法,會串聯方法或是語意,都會比原本的 tap()
函式來的好。
toBase()
這個方法是用在繼承 Collection 的類別如 Eloquent Collection,如果想轉成單純的 Collection 的話,可以使用這個。
public function toBase() |
曾有自定義類別是繼承 Collection 的,不過設計上比較單純,所以還沒用過這個方法。
wrap()
與 unwrap()
wrap()
會使用 Collection 把傳入的參數包裝起來,unwrap()
則是解包裝。
public static function wrap($value) |
其中會注意到,warp()
裡面還用到了 Arr::wrap()
,因此非 array 的參數也可以正常使用。
// class Arr |
when()
與 unless()
這兩個方法很有趣,when()
是當 $value 是 true 的時候,就會執行 callback;unless()
則相反。
public function when($value, callable $callback, callable $default = null) |
可以注意到上面 unless()
寫法很有趣,它把 $value 反相後丟到 when,即可做出 unless 的方法。這特性在 Laravel 也很常見,比方說 isEmpty()
與 isNotEmpty()
public function isEmpty() |
可以看到 isNotEmpty()
其實就是 isEmpty()
的相反。
會這樣設計的原因是:用起來比較直覺語意,可以參考下面兩段程式碼即可了解:
if (!$collection->isEmpty()) { |
Laravel 框架很多設計都是環繞在直覺語意上的,非常值得參考。