跳到內容

HttpKernel 組件

編輯此頁面

HttpKernel 組件透過使用 EventDispatcher 組件,提供結構化的流程,將 Request 轉換為 Response。它具有足夠的彈性來建立完整堆疊的框架 (Symfony) 或進階的 CMS (Drupal)。

安裝

1
$ composer require symfony/http-kernel

注意

如果您在 Symfony 應用程式之外安裝此組件,您必須在程式碼中引入 vendor/autoload.php 檔案,以啟用 Composer 提供的類別自動載入機制。閱讀這篇文章以瞭解更多細節。

請求-回應生命週期

另請參閱

本文說明如何在任何 PHP 應用程式中將 HttpKernel 功能作為獨立組件使用。在 Symfony 應用程式中,一切都已設定完成並可直接使用。閱讀控制器事件和事件監聽器文章,以瞭解如何在 Symfony 應用程式中使用它來建立控制器和定義事件。

每個 HTTP 網路互動都始於請求,終於回應。身為開發人員,您的工作是建立 PHP 程式碼,讀取請求資訊(例如 URL),並建立和傳回回應(例如 HTML 頁面或 JSON 字串)。這是 Symfony 應用程式中請求-回應生命週期的簡化概述

  1. 使用者瀏覽器中請求資源
  2. 瀏覽器請求傳送至伺服器
  3. Symfony 給予應用程式一個 Request 物件;
  4. 應用程式使用 Request 物件的資料產生 Response 物件;
  5. 伺服器回應傳回給瀏覽器
  6. 瀏覽器使用者顯示資源

通常,會建置某種框架或系統來處理所有重複性任務(例如路由、安全性等),以便開發人員可以建置應用程式的每個頁面。這些系統如何建置差異很大。HttpKernel 組件提供了一個介面,將從請求開始並建立適當回應的過程形式化。此組件旨在成為任何應用程式或框架的核心,無論該系統的架構如何變化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;

interface HttpKernelInterface
{
    // ...

    /**
     * @return Response A Response instance
     */
    public function handle(
        Request $request,
        int $type = self::MAIN_REQUEST,
        bool $catch = true
    ): Response;
}

在內部,HttpKernel::handle() - HttpKernelInterface::handle() 的具體實作 - 定義了一個生命週期,該生命週期始於 Request 並終於 Response

此生命週期的確切細節是了解核心(以及 Symfony 框架或任何其他使用核心的程式庫)如何運作的關鍵。

HttpKernel:由事件驅動

HttpKernel::handle() 方法透過分發事件在內部運作。這使得該方法既靈活又有點抽象,因為使用 HttpKernel 建置的框架/應用程式的所有「工作」實際上都是在事件監聽器中完成的。

為了幫助解釋這個過程,本文將著眼於過程的每個步驟,並討論 HttpKernel 的一個特定實作 - Symfony 框架 - 如何運作。

最初,使用 HttpKernel 不需要太多步驟。您建立一個事件分發器和一個控制器和參數解析器(如下所述)。為了完成您的運作核心,您將為下面討論的事件新增更多事件監聽器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\HttpKernel;

// create the Request object
$request = Request::createFromGlobals();

$dispatcher = new EventDispatcher();
// ... add some event listeners

// create your controller and argument resolvers
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

// instantiate the kernel
$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);

// actually execute the kernel, which turns the request into a response
// by dispatching events, calling a controller, and returning the response
$response = $kernel->handle($request);

// send the headers and echo the content
$response->send();

// trigger the kernel.terminate event
$kernel->terminate($request, $response);

請參閱「完整的運作範例」以取得更具體的實作。

有關將監聽器新增至以下事件的一般資訊,請參閱建立事件監聽器

另請參閱

有一個關於使用 HttpKernel 組件和其他 Symfony 組件來建立您自己的框架的精彩教學系列。請參閱簡介

1) kernel.request 事件

典型用途:向 Request 新增更多資訊、初始化系統的各個部分,或在可能時傳回 Response(例如,拒絕存取的安全性層)。

核心事件資訊表

HttpKernel::handle 內部分發的第一個事件是 kernel.request,它可能具有各種不同的監聽器。

此事件的監聽器可能非常多樣化。某些監聽器(例如安全性監聽器)可能具有足夠的資訊來立即建立 Response 物件。例如,如果安全性監聽器判斷使用者沒有存取權,則該監聽器可能會傳回 RedirectResponse 至登入頁面或 403 拒絕存取回應。

如果在此階段傳回 Response,則程序將直接跳至 kernel.response 事件。

其他監聽器會初始化事物或向請求新增更多資訊。例如,監聽器可能會判斷並在 Request 物件上設定語言環境。

另一個常見的監聽器是路由。路由器監聽器可能會處理 Request 並判斷應呈現的控制器(請參閱下一節)。實際上,Request 物件具有「屬性」包,這是儲存有關請求的額外應用程式特定資料的完美位置。這表示如果您的路由器監聽器以某種方式判斷控制器,則它可以將其儲存在 Request 屬性上(您的控制器解析器可以使用它)。

總體而言,kernel.request 事件的目的是直接建立並傳回 Response,或向 Request 新增資訊(例如設定語言環境或在 Request 屬性上設定其他資訊)。

注意

kernel.request 事件設定回應時,傳播會停止。這表示優先權較低的監聽器將不會執行。

Symfony 框架中 kernel.request 最重要的監聽器是 RouterListener。此類別執行路由層,該路由層傳回有關相符請求的陣列資訊,包括 _controller 和路由模式中的任何預留位置(例如 {slug})。請參閱路由文件

此資訊陣列儲存在 Request 物件的 attributes 陣列中。在此處新增路由資訊目前不會執行任何操作,但會在解析控制器時接著使用。

2) 解析控制器

假設沒有 kernel.request 監聽器能夠建立 Response,則 HttpKernel 中的下一步是判斷和準備(即解析)控制器。控制器是最終應用程式程式碼的一部分,負責為特定頁面建立和傳回 Response。唯一的要求是它是 PHP 可呼叫物件 - 即函式、物件上的方法或 Closure

但是,如何判斷請求的確切控制器完全取決於您的應用程式。這是「控制器解析器」的工作 - 一個實作 ControllerResolverInterface 的類別,並且是 HttpKernel 的建構函式引數之一。

您的工作是建立一個實作介面的類別,並填寫其方法:getController()。實際上,已經存在一個預設實作,您可以直接使用或從中學習:ControllerResolver。此實作在下面的側邊欄中進行了更多說明

1
2
3
4
5
6
7
8
namespace Symfony\Component\HttpKernel\Controller;

use Symfony\Component\HttpFoundation\Request;

interface ControllerResolverInterface
{
    public function getController(Request $request): callable|false;
}

在內部,HttpKernel::handle() 方法首先在控制器解析器上呼叫 getController()。此方法會傳遞 Request,並負責以某種方式根據請求的資訊判斷並傳回 PHP 可呼叫物件(控制器)。

Symfony 框架使用內建的 ControllerResolver 類別(實際上,它使用一個子類別,其中包含下面提到的一些額外功能)。此類別利用在 RouterListener 期間放置在 Request 物件的 attributes 屬性上的資訊。

getController

ControllerResolverRequest 物件的屬性屬性上尋找 _controller 索引鍵(回想一下,此資訊通常透過 RouterListener 放置在 Request 上)。然後,透過執行以下操作,將此字串轉換為 PHP 可呼叫物件

a) 如果 _controller 索引鍵不遵循建議的 PHP 命名空間
格式(例如 App\Controller\DefaultController::index),則其格式會轉換為該格式。例如,舊版 FooBundle:Default:index 格式將變更為 Acme\FooBundle\Controller\DefaultController::indexAction。此轉換特定於 Symfony 框架使用的 ControllerResolver 子類別。
b) 您的控制器類別的新執行個體將在沒有
建構函式引數的情況下執行個體化。

3) kernel.controller 事件

典型用途:初始化事物或在控制器執行之前變更控制器。

核心事件資訊表

在判斷控制器可呼叫物件後,HttpKernel::handle() 會分發 kernel.controller 事件。此事件的監聽器可能會初始化系統的某些部分,這些部分需要在某些事物(例如控制器、路由資訊)已判斷但控制器尚未執行之後初始化。

此事件的另一個典型用例是使用 getAttributes() 方法從控制器擷取屬性。有關一些範例,請參閱下面的 Symfony 區段。

此事件的監聽器也可以透過在傳遞給此事件監聽器的事件物件上呼叫 ControllerEvent::setController 來完全變更控制器可呼叫物件。

Symfony 框架中 kernel.controller 的一個有趣的監聽器是 CacheAttributeListener。此類別從控制器擷取 #[Cache] 屬性組態,並使用它在回應上組態HTTP 快取

Symfony 框架中還有一些其他次要的 kernel.controller 事件監聽器,用於在啟用分析器時處理收集分析器資料。

4) 取得控制器參數

接下來,HttpKernel::handle() 呼叫 ArgumentResolverInterface::getArguments()。請記住,在 getController() 中傳回的控制器是可呼叫物件。getArguments() 的目的是傳回應傳遞給該控制器的引數陣列。雖然內建的 ArgumentResolver 是一個很好的範例,但確切的做法完全取決於您的設計。

此時,核心具有 PHP 可呼叫物件(控制器)和在執行該可呼叫物件時應傳遞的引數陣列。

現在您確切知道控制器可呼叫物件(通常是控制器物件內的方法)是什麼,ArgumentResolver 在可呼叫物件上使用反射,以傳回每個引數的名稱陣列。然後,它會反覆運算每個引數,並使用以下技巧來判斷應為每個引數傳遞哪個值

a) 如果 Request 屬性包包含與名稱相符的索引鍵
的引數,則使用該值。例如,如果控制器的第一個引數是 $slug,並且 Request attributes 包中有 slug 索引鍵,則會使用該值(而且此值通常來自 RouterListener)。
b) 如果控制器中的引數使用 Symfony 的類型提示
Request 物件,則 Request 作為值傳入。
c) 如果函式或方法引數是可變引數,並且 Request
attributes 包包含該引數的陣列,則它們都將透過可變引數引數提供。

此功能由實作 ValueResolverInterface 的解析器提供。有四個實作提供 Symfony 的預設行為,但自訂是此處的關鍵。透過自行實作 ValueResolverInterface 並將其傳遞給 ArgumentResolver,您可以擴充此功能。

5) 呼叫控制器

HttpKernel::handle() 的下一步是執行控制器。

控制器的職責是為給定的資源建置回應。這可以是 HTML 頁面、JSON 字串或任何其他內容。與到目前為止程序的每個其他部分不同,此步驟由「最終開發人員」針對建置的每個頁面實作。

通常,控制器會回傳一個 Response 物件。如果這是真的,那麼 Kernel 的工作就差不多完成了!在這種情況下,下一步是 kernel.response 事件。

但是,如果控制器回傳的不是 Response,那麼 Kernel 還有更多工作要做 - kernel.view (因為最終目標是始終產生一個 Response 物件)。

注意

控制器必須回傳某些東西。如果控制器回傳 null,將會立即拋出例外。

6) kernel.view 事件

典型用途:將來自控制器的非 Response 回傳值轉換為 Response

核心事件資訊表

如果控制器沒有回傳 Response 物件,那麼 Kernel 會分派另一個事件 - kernel.view。此事件的監聽器的工作是使用控制器的回傳值(例如,資料陣列或物件)來建立一個 Response

如果您想使用「視圖」層,這會很有用:您可以從控制器回傳代表頁面的資料,而不是回傳 Response。此事件的監聽器接著可以使用這些資料來建立格式正確的 Response(例如 HTML、JSON 等)。

在這個階段,如果沒有監聽器在此事件上設定回應,則會拋出例外:控制器其中一個視圖監聽器必須始終回傳 Response

注意

當為 kernel.view 事件設定回應時,傳播會停止。這表示優先順序較低的監聽器將不會被執行。

在 Symfony 框架內,有一個用於 kernel.view 事件的預設監聽器。如果您的控制器操作回傳一個陣列,並且您將 #[Template] attribute 應用於該控制器操作,那麼此監聽器會渲染一個模板,將您從控制器回傳的陣列傳遞給該模板,並建立一個包含該模板回傳內容的 Response

此外,一個流行的社群套件 FOSRestBundle 在此事件上實作了一個監聽器,旨在為您提供一個強大的視圖層,能夠使用單一控制器回傳許多不同的內容類型回應(例如 HTML、JSON、XML 等)。

7) kernel.response 事件

典型用途:在 Response 物件傳送之前修改它

核心事件資訊表

Kernel 的最終目標是將 Request 轉換為 ResponseResponse 可能在 kernel.request 事件期間建立,從 控制器 回傳,或由 kernel.view 事件的監聽器之一回傳。

無論誰建立 Response,另一個事件 - kernel.response 會在之後立即分派。此事件的典型監聽器將以某種方式修改 Response 物件,例如修改標頭、新增 Cookie,甚至變更 Response 本身的內容(例如,在 HTML 回應的結尾 </body> 標籤之前注入一些 JavaScript)。

在此事件分派之後,最終的 Response 物件會從 handle() 回傳。在最典型的用例中,您可以接著呼叫 send() 方法,該方法會傳送標頭並印出 Response 內容。

在 Symfony 框架內部,此事件上有幾個次要的監聽器,大多數都會以某種方式修改回應。例如,WebDebugToolbarListener 會在 dev 環境中將一些 JavaScript 注入到頁面底部,這會導致顯示 Web 除錯工具列。另一個監聽器 ContextListener 會將目前使用者的資訊序列化到 Session 中,以便在下一個請求中重新載入。

8) kernel.terminate 事件

典型用途:在回應串流傳輸到使用者後執行一些「繁重」的操作

核心事件資訊表

HttpKernel 流程的最後一個事件是 kernel.terminate,它是獨特的,因為它發生在 HttpKernel::handle() 方法之後,以及在回應傳送給使用者之後。回想一下上面的內容,然後使用 Kernel 的程式碼會像這樣結束

1
2
3
4
5
// sends the headers and echoes the content
$response->send();

// triggers the kernel.terminate event
$kernel->terminate($request, $response);

如您所見,透過在傳送回應後呼叫 $kernel->terminate,您將觸發 kernel.terminate 事件,您可以在其中執行某些您可能延遲的操作,以便盡快將回應回傳給用戶端(例如,傳送電子郵件)。

警告

在內部,HttpKernel 使用了 fastcgi_finish_request PHP 函數。這表示目前只有 PHP FPM 伺服器 API 能夠在伺服器的 PHP 處理程序仍在執行某些任務時將回應傳送給用戶端。對於所有其他伺服器 API,kernel.terminate 的監聽器仍然會被執行,但回應要等到它們全部完成後才會傳送給用戶端。

注意

使用 kernel.terminate 事件是選用的,並且僅應在您的 Kernel 實作 TerminableInterface 時才應呼叫。

9) 處理例外:kernel.exception 事件

典型用途:處理某些類型的例外,並建立適當的 Response 以針對例外回傳

核心事件資訊表

如果在 HttpKernel::handle() 內部任何時間點拋出例外,則會分派另一個事件 - kernel.exception。在內部,handle() 方法的主體被包裝在 try-catch 區塊中。當拋出任何例外時,會分派 kernel.exception 事件,以便您的系統可以以某種方式回應例外。

此事件的每個監聽器都會傳遞一個 ExceptionEvent 物件,您可以使用它透過 getThrowable() 方法存取原始例外。此事件上的典型監聽器將檢查特定類型的例外,並建立適當的錯誤 Response

例如,為了產生 404 頁面,您可能會拋出特殊類型的例外,然後在此事件上新增一個監聽器,該監聽器會尋找此例外,並建立及回傳 404 Response。事實上,HttpKernel 元件帶有一個 ErrorListener,如果您選擇使用它,預設情況下會執行此操作以及更多操作(請參閱下面的側邊欄以取得更多詳細資訊)。

ExceptionEvent 公開了 isKernelTerminating() 方法,您可以使用它來判斷 Kernel 在拋出例外時是否正在終止。

7.1

isKernelTerminating() 方法是在 Symfony 7.1 中引入的。

注意

當為 kernel.exception 事件設定回應時,傳播會停止。這表示優先順序較低的監聽器將不會被執行。

在使用 Symfony 框架時,kernel.exception 有兩個主要的監聽器。

HttpKernel 元件中的 ErrorListener

第一個是 HttpKernel 元件的核心,稱為 ErrorListener。此監聽器有幾個目標

  1. 拋出的例外會被轉換為 FlattenException 物件,其中包含有關請求的所有資訊,但可以列印和序列化。
  2. 如果原始例外實作了 HttpExceptionInterface,則會在例外上呼叫 getStatusCode()getHeaders(),並用於填入 FlattenException 物件的標頭和狀態碼。想法是這些會在建立最終回應的下一步中使用。如果您想設定自訂 HTTP 標頭,您可以隨時在衍生自 HttpException 類別的例外上使用 setHeaders() 方法。
  3. 如果原始例外實作了 RequestExceptionInterface,則 FlattenException 物件的狀態碼會填入 400,並且不會修改其他標頭。
  4. 將執行控制器並將扁平化的例外傳遞給它。要渲染的確切控制器會作為建構子引數傳遞給此監聽器。此控制器將為此錯誤頁面回傳最終的 Response

Security 元件中的 ExceptionListener

另一個重要的監聽器是 ExceptionListener。此監聽器的目標是處理安全性例外,並在適當的時候協助使用者進行身份驗證(例如,重新導向到登入頁面)。

建立事件監聽器

如您所見,您可以建立事件監聽器並將其附加到 HttpKernel::handle() 週期期間分派的任何事件。通常,監聽器是一個帶有要執行的方法的 PHP 類別,但它可以是任何東西。有關建立和附加事件監聽器的更多資訊,請參閱 EventDispatcher 元件

每個「Kernel」事件的名稱都定義為 KernelEvents 類別上的常數。此外,每個事件監聽器都會傳遞一個單一引數,它是 KernelEvent 的一些子類別。此物件包含有關系統目前狀態的資訊,並且每個事件都有自己的事件物件

名稱 KernelEvents 常數 傳遞給監聽器的引數
kernel.request KernelEvents::REQUEST RequestEvent
kernel.controller KernelEvents::CONTROLLER ControllerEvent
kernel.controller_arguments KernelEvents::CONTROLLER_ARGUMENTS ControllerArgumentsEvent
kernel.view KernelEvents::VIEW ViewEvent
kernel.response KernelEvents::RESPONSE ResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE TerminateEvent
kernel.exception KernelEvents::EXCEPTION ExceptionEvent

完整的運作範例

當使用 HttpKernel 元件時,您可以自由地將任何監聽器附加到核心事件,使用任何實作 ControllerResolverInterface 的控制器解析器,以及使用任何實作 ArgumentResolverInterface 的引數解析器。但是,HttpKernel 元件帶有一些內建的監聽器以及其他可用於建立工作範例的所有東西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', [
    '_controller' => function (Request $request): Response {
        return new Response(
            sprintf("Hello %s", $request->get('name'))
        );
    }]
));

$request = Request::createFromGlobals();

$matcher = new UrlMatcher($routes, new RequestContext());

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack()));

$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

子請求

除了傳送到 HttpKernel::handle() 的「主要」請求之外,您還可以傳送所謂的「子請求」。子請求的外觀和行為都像任何其他請求,但通常僅用於渲染頁面的一小部分,而不是整個頁面。您最常從控制器(或可能從控制器正在渲染的模板內部)發出子請求。

若要執行子請求,請使用 HttpKernel::handle(),但將第二個引數變更如下

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

// ...

// create some other request manually as needed
$request = new Request();
// for example, possibly set its _controller manually
$request->attributes->set('_controller', '...');

$response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST);
// do something with this response

這會建立另一個完整的請求-回應週期,其中這個新的 Request 會轉換為 Response。內部的唯一區別在於,某些監聽器(例如安全性)可能僅對主要請求起作用。每個監聽器都會傳遞 KernelEvent 的某些子類別,其 isMainRequest() 方法可用於檢查目前的請求是「主要」請求還是「子」請求。

例如,只需要對主要請求起作用的監聽器可能看起來像這樣

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpKernel\Event\RequestEvent;
// ...

public function onKernelRequest(RequestEvent $event): void
{
    if (!$event->isMainRequest()) {
        return;
    }

    // ...
}

注意

_format 請求屬性的預設值是 html。如果您的子請求回傳不同的格式(例如 json),您可以透過在請求上明確定義 _format 屬性來設定它

1
$request->attributes->set('_format', 'json');

定位資源

HttpKernel 元件負責 Symfony 應用程式中使用的套件機制。套件的關鍵功能之一是,您可以使用邏輯路徑而不是實體路徑來參照它們的任何資源(組態檔、模板、控制器、翻譯檔等)。

即使您不知道套件將安裝在檔案系統中的哪個位置,也可以匯入資源。例如,儲存在名為 FooBundle 的套件的 Resources/config/ 目錄中的 services.xml 檔案可以參照為 @FooBundle/Resources/config/services.xml 而不是 __DIR__/Resources/config/services.xml

這要歸功於 Kernel 提供的 locateResource() 方法,該方法將邏輯路徑轉換為實體路徑

1
$path = $kernel->locateResource('@FooBundle/Resources/config/services.xml');
這項作品,包括程式碼範例,均根據 Creative Commons BY-SA 3.0 授權條款授權。
TOC
    版本