分析 bootstrap 流程
一開始,我們先來了解 Laravel 從 process 開出來後,到進 Controller 前到底做了哪些事。
了解這些會有助於我們理解 Laravel 元件是如何初始化的。
從進入點開始
所有 web 程式的進入點(entry point),就是 index.php
。這個檔案主要做的事如下:
$app = require_once __DIR__.'/../bootstrap/app.php'; |
Application 是 Laravel 整個生命週期都會使用到的 Service Container,當需要產生物件的時候,都會需要它的幫忙。
而建構的方法就寫在 bootstrap/app.php
裡,主要就做兩件事:設定主要目錄 與 綁定實作。
$app = new Illuminate\Foundation\Application( |
設定主要目錄是因為,後面有很多任務都需要找子目錄,而這些子目錄都相對於主要目錄。而綁定實作後,之後可以依據不同情境,去透過 Application 建置需要的實例來使用。
這是一個很聰明的做法。
現代化的網頁應用,除了提供網頁服務外,有時也會提供 CLI 或是測試等不同使用情境;通常也會希望指令能使用網頁服務的程式碼,或是測試能真正測到實際網頁服務的程式碼。而只要 Application 的初始化一致,即可讓不同情境所使用的程式碼一致。
這個做法同樣可以應用在「Container」與「處理 Http 的角色」是分離的框架上,如:
- Slim Framework 的
Slim\Container
與Slim\App
- Phalcon 的
Phalcon\Di
與Phalcon\Mvc\Application
綁定實作之後會在分析 Container 的時候說明細節。
拿到 Application 後,繼續 index.php
的任務
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); |
Application 第一個生產任務就是 Http Kernel。Http Kernel 正如其名,是處理 Http 的核心。
$response = $kernel->handle( |
這裡使用 handle()
處理 Illuminate\Http\Request
物件。
$response->send(); |
呼叫 Symfony\Component\HttpFoundation\Response
的 send()
,這將會把 response 裡所存放的 header 與 content 輸出到瀏覽器。
在這之前,對 response 所做的任何操作,都只是在記憶體運作,而不會有任何輸出。
$kernel->terminate($request, $response); |
最後呼叫 Http Kernel 的 terminate()
,它其實沒做特別的事,主要是在觸發 terminate 「事件」。它並不是用 Event 實作,而是直接觸發 Middleware 的 terminate()
與 Application 的 terminatingCallbacks
屬性上。
Http Kernel 做了些什麼
再來我們肯定會很好奇,那 request 到底是進到什麼樣的黑盒子,才轉成 response 呢?這就要繼續往 Http Kernel 追了。
首先,先看它的建構子,是在設定一些參數,其中 Illuminate\Routing\Router
正是實作 Routing 的核心。
關係圖如下:
PlantUML 原始碼:
@startuml |
類別圖錯字已修正,感謝 Yi-hsuan Lai 提醒。
接著 handle()
才是真正做事的地方,也就是剛剛在 index.php
看到的那個被呼叫的方法。其中有一行,是產生 response 的地方:
$response = $this->sendRequestThroughRouter($request); |
再進去 sendRequestThroughRouter()
,看看它做了什麼事:
// 把 request 設定到 Container |
如何分配任務給正確的 Controller,將會是 [Routing][] 的任務,這等未來提到的時候再討論。
我們先把焦點先放在 bootstrap()
做了什麼吧!
看原始碼可以發現,首先會判斷如果曾經 bootstrap 過,就不會做事。
這是因為,對傳統 PHP 來說,每次的 request 都會重新建立 process 並重新 bootstrap,但 Laravel 的 Feature 測試是可以在一個測試打多個 request,每次 bootstrap 豈不慢到爆炸,所以才會有這樣的設計。
而 bootstrapWith()
是把 $bootstrappers
拿來都 bootstrap 一下,內容如下:
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, |
從這些 class 名稱,可以大概知道它依續做了這些事:
- 載入 .env
- 載入 config 設定
- 設定 error handle
- 設定 Facade
- 註冊 Service Provider
- 啟動 Service Provider
而從這個順序就可以發現下面這些事
- 在 config 裡可以正常使用
env()
拿環境變數,因為LoadEnvironmentVariables
先執行 - Provider 可以正常拿取
config()
設定,因為LoadConfiguration
先執行 - Provider 也可以正常使用 Facade,因為
RegisterFacades
先執行 - Provider 的
register()
會比boot()
先執行 - Provider 炸掉會正確地被 error handler 接到,因為
HandleExceptions
先執行
這個順序是定義在 Kernel 的 property,所以意味著它可以被覆寫。比方說我們可能需要使用 YAML 設定檔,則可以加入一個 \App\Bootstrap\LoadYamlConfiguration::class
來負責載入 YAML 設定。
artisan
index.php
是 web 的進入點,而 artisan
指令則是 cli 的進入點。
內容大同小異,一樣是把 Application 建構好後,再換拿 Console Kernel。跟 Http Kernel 一樣,會有一個 handle()
方法在處理所有事情,不過對 console 而言,需要的參數是 I/O。最後一樣也有 terminate()
,不一樣的是多了 exit($status)
,這是因為對 cli 來說,一個指令的結束,會需要回傳一個狀態碼,而這任務是由 exit()
function 達成。
handle()
實作很簡單:bootstrap、getArtisan、run。其中 Artisan 比較複雜,未來有機會再來討論。
bootstrap()
實作比 Http Kernel 多了幾件事:
$this->app->loadDeferredProviders(); |
Application 的 loadDeferredProviders()
方法是把原本要延遲載入的 provider 一次性的全載進來。
commands()
則是用在 Closure commands 上,因為官方說明是使用 Artisan facade 來註冊 Closure commands。對 Kernel 來說,只要 Artisan 的生命週期還在,這邊就不需要再次呼叫 commands()
,所以就出現類似 hasBeenBootstrapped()
的判斷寫法。
今日總結
以上,是 Laravel 在進到商業邏輯層(Controller / Command)前的程式碼分析,同時也描述了一小部分的 lifecycle。
了解之後,接下來在看某幾個跟流程初始化有關的元件,就會比較好理解為何它能正常運作。同時,也可以知道 Laravel 如何做到調整初始化流程,與了解它彈性的設計。