Day 10,筆者提到了 PSR 規範列表中,目前正運作的有 13 個,而 Day 11,筆者介紹了 PHP 程式碼風格 PSR-1, 12 及 Day 12 的 PHP 自動載入機制 PSR-4。而本篇是本系列最後一篇關於 PSR 的文章,介紹的是和 HTTP 通訊處理和伺服器端相關的規範如下:

編號 名稱 名稱 (中) 簡介
7 HTTP message interfaces  HTTP 通訊介面 HTTP 訊息規範於 RFC 7230RFC 7231,URL 規範於 RFC 3986 的實作建議。
15 HTTP Handlers HTTP 處理程式 HTTP 伺服器端請求處理程式,為 PSR-7 提供了中介層 (middleware) 的實作規範
17 HTTP Factories HTTP 工廠 為 PSR-7 的工廠模式。

PSR 已經是現代先進框架 (Modern framework) 的共同標準了,瞭解了這些等於同時懂了好幾套框架對於 HTTP 的處理方法,例如 Laravel、Zend、Slim 等等。差別在有些框架會加料,除了介面定義的方法外,還額外自定框架所需的方法,但基本方法都通用。

如果專案作品中含有對 HTTP 處理,最好的實踐是相容於 PSR-7,利用 PSR-7 包裝的抽象方法來處理,以避免使用 PHP 原生函式可能會產生的衝突問題。這也是筆者在這一系列文章中特別提到此篇文章主題的原因,畢竟要發表作品給大眾使用,總是要多方事先考慮到可能發生問題的情況才對。

不同於之前筆者在介紹 PSR 時採用翻譯原文並整理內容的方式,在此篇僅列出如何使用以及注意的細節。

PSR-7:HTTP 通訊介面

PSR-7 是給想造輪子的開發者有個標準可依循,以相容同樣採用此規範的框架。其它採用 PSR-7 的套件也能彼此相容。

界面

介面名稱 繼承 方法數 簡介
MessageInterface (A) - 11 HTTP 的通訊由客戶端的請求和伺服器端的回應所組成。此界面定義了通用的方法。(RFC 7230, RFC 7231)
RequestInterface (B) (A) 6 表示一個對外發出的客戶端 HTTP 請求。
ServerRequestInterface (B) 13 表示一個接收進來的伺服器端 HTTP 請求。
ResponseInterface (A) 3 表示一個對外發出的伺服器端 HTTP 回應。
StreamInterface - 15 將資料流處理包裝成一個實例物件,並提供抽象方法來操作資料流。
UriInterface - 16 依 RFC 3986 來把 URI 處理包裝成實例物件,並提供抽象方法來操作 URI,主要使用範圍在 HTTP 請求,但也可以用其它地方。
UploadedFileInterface - 6 把檔案上傳的超全域變數 $_FILES 包裝成物件,並提供抽象方法來操作檔案。

現有的 PSR-7 實作套件如下:

供應商命名空間 GiHub 專案連結 授權條款 簡介
GuzzleHttp https://github.com/guzzle/psr7 MIT 最早實作 PSR-7 的套件。
Slim https://github.com/slimphp/Slim-Psr7 MIT Slim 4 框架所用的 PSR-7 套件。
Shieldon https://github.com/terrylinooo/psr-http MIT 使用於 Shieldon WAF (web application firewall) 的 PSR-7 套件。

其中 Shieldon PSR-HTTP 這個套件是筆者實作,已經有現成的套件可使用的話理當不需要重造輪子,但為了用在自己的作品 Shieldon 中,仔細的審閱了各個 PSR-7 套件,讓我有了重造輪子的念頭,理由如下:

  • PSR-7 套件應單純的實作規範,GuzzleHttp 的 PSR-7 套件大量依賴自己寫的 helper 函式,且 Messages 使用特性而非界面,沒完全照規範實作。
  • Slim PSR-7 的品質佳,但筆者考慮到如採用該套件則必須額外花費心力去追其套件更新的細節。筆者也認為既然已經有規範出界面,整個主目錄應就個別的類別對一個界面,讓結構更清楚。

因此筆者按照 PSR-7 規範,詳讀研究 RFC 7230,RFC 7231,RFC 3986 規範文件後實作。結構如下:

ecvMRpL.png

上圖為 PSR-7 各介面定義的基本方法。大部份的 PHP 框架除了這些基本方法以外,還會自訂其它方法以供框架使用。但基本方法在各框架都是通用的。

PSR-15: HTTP 請求處理器

在 PSR-7 規範問世之後,各路高手紛紛使用 PSR-7 大展身手,結果變成在中介層 (middleware) 的應用上出現了好幾套不同的設計模式,不同的框架在中介層的類別無法直接套用到其它框架,複用性不是很理想。為了解決這種情況,於是 PSR-15 規範出兩個介面,RequestHandlerInterfaceMiddlewareInterface

RequestHandlerInterface

interface RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request): ResponseInterface;
}

MiddlewareInterface

interface MiddlewareInterface
{
    public function process(
        ServerRequestInterface  $request,
        RequestHandlerInterface $handler
    ): ResponseInterface;
}

一般而言,PHP 框架皆已內建 RequestHandler,所以只要是專案作品是會處理到 HTTP 請求的部分,皆從中介層下手。

範例:

class RejectNoUseragent implements MiddlewareInterface
{
    /**
     * Invoker.
     *
     * @param ServerRequestInterface  $request The PSR-7 server request.
     * @param RequestHandlerInterface $handler The PSR-15 request handler.
     *
     * @return ResponseInterface
     */
    public function process(
        ServerRequestInterface  $request,
        RequestHandlerInterface $handler): ResponseInterface
    {
        if (!$request->hasHeader('user-agent')) {
            return (new Response)->withStatus(400);
        }

        return $handler->handle($request);
    }
}

這是一個簡易版的中介層範例,用途為:進來的 HTTP 請求沒有 User-Agent 資訊,則回用 400 (Bad Request) 給客戶端。

$handler->handle($request);

如果有含 User-Agent,那就繼續下一個中介層。

PSR-17: HTTP 工廠

由於創建 PSR-7 中的各類別實例化物件的時候,要另外寫程式碼帶參數,免不了寫多行程式。這種情況很適合使用筆者在 Day 7 介紹的工廠模式 來創造物件。

以下是取自 Shieldon PSR-HTTP 的範例:

$serverRequest = ServerRequestFactory::fromGlobal();

建立一個 serverRequest 的物件。

$response = ResponseFactory::fromNew();

建立一個 response 的物件。

如此一來很方便。要注意的是 PSR-15 定義的介面方法太陽春,以致於各家 PSR-15 實作,參數都不一樣!
因此都還得另外定義方法(大部分都是靜態方法 fromGlobal)。因此在採用 PSR-15 工廠時,注意一下套件供應商的文件,看看使用方法。

注意事項

  • 由於 PSR-7 已經將超全域變數 $_GET$_COOKIE, $_POST, $_FILES 封裝進 ServerRequest 物件中使用。不得再直接對這些超全域變數進行操作。

  • 不得使用像是 session_start、setcookie 這類函式,如圖:

jLa0nOA.png

正確的流程是組裝完 Response 物件後,由解析器統一輸出,把設定 Cookie 等操作集中在輸出前由 header() 函式行 header 輸出。使用 session_startsetcookie 的同時,就提前輸出 header 了,破壞流程往後會有意想不到的問題得抓漏。

Session 的使用須棄用 PHP 原生的 $_SESSION 及相關函式,改用自定的 session driver。相信各框架皆以內建。所以別再用原生的 Session 囉。

總結

當我們開源的作品要盡可能相容於各先進框架時:

  • 如有 HTTP 操作,請採用 PSR-7。並將邏輯寫在 PSR-15 中介層中,是不錯的作法。
  • 注意避免使用會提前輸出的函式,例如 session_start、setcookie。以符合此設計模式。
  • 假如作品中沒有任何 HTTP 操作,那麼此篇文章可以跳過囉!

終於結束了一些前置性介紹觀念的文章,明天開始就要開始我們的第一個 Composer 套件實作流程囉。我們明天見囉!

最後修改日期: 2022-02-02

作者

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。