中間件

您可以在 Slim 應用程式之前之後執行程式碼,以自行設定請求和回應物件。這稱為中間件。為什麼您想這麼做?可能是您想保護您的 app 免於跨網站請求偽造。也可能是您想在 app 執行之前驗證請求。在這些情況下,中間件就是完美的選擇。

什麼是中間件?

中間件是一個位於網頁應用程式中,於客戶端請求和伺服器回應之間的層級。當它們穿過應用程式管線時,中間件會攔截、處理,並可能更動 HTTP 請求和回應。

中間件可以處理驗證、授權、記錄、請求修改、回應轉換、錯誤處理等多項任務。

每個中間件執行其功能,然後將控制權傳遞給鏈中的下一個中間件,這使得在網頁應用程式中處理橫切關注項時,能夠採模組化且可重複使用的做法。

中間件如何運作?

不同的架構會以不同的方式使用中間件。Slim 會在核心應用程式外圍新增中間件作為同心圓形狀的層級。每個新的中間件層級會包圍現有的任何中間件層級。同心圓形的結構會隨著加入更多中間件層級而向外擴張。

最後新增的中介層會先執行。

您執行 Slim 應用程式時,Request 物件會由外往內穿透中介層結構。它們會先進入最外層的中介層,然後進入次外層的中介層(依此類推),直到最後抵達 Slim 應用程式本身。在 Slim 應用程式指派適當的路由之後,結果的 Response 物件會離開 Slim 應用程式並由內往外穿透中介層結構。最後,一個最終的 Response 物件會離開最外層的中介層,序列號化成為一個原始 HTTP 回應,並傳回 HTTP 客戶端。以下是說明中介層程序流程的圖表

Middleware architecture

我如何撰寫中介層?

中介層是一個可呼叫的,接受兩個參數的物件:一個 Request 物件和一個 RequestHandler 物件。每個中介層 必須傳回 Psr\Http\Message\ResponseInterface 的實例。

閉包中介層

這個中介層範例是一個閉包。

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$beforeMiddleware = function (Request $request, RequestHandler $handler) use ($app) {
    // Example: Check for a specific header before proceeding
    $auth = $request->getHeaderLine('Authorization');
    if (!$auth) {
        // Short-circuit and return a response immediately
        $response = $app->getResponseFactory()->createResponse();
        $response->getBody()->write('Unauthorized');
        
        return $response->withStatus(401);
    }

    // Proceed with the next middleware
    return $handler->handle($request);
};

$afterMiddleware = function (Request $request, RequestHandler $handler) {
    // Proceed with the next middleware
    $response = $handler->handle($request);
    
    // Modify the response after the application has processed the request
    $response = $response->withHeader('X-Added-Header', 'some-value');
    
    return $response;
};

$app->add($afterMiddleware);
$app->add($beforeMiddleware);

// ...

$app->run();

可呼叫類別中介層

這個中介層範例是一個可呼叫的類別,它實作了神奇的 __invoke() 方法。

<?php

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Psr\Http\Message\ResponseInterface as Response;

class ExampleBeforeMiddleware
{
    public function __invoke(Request $request, RequestHandler $handler): Response
    {
        // Handle the incoming request
        // ...

        // Invoke the next middleware and return response
        return $handler->handle($request);
    }
}
<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class ExampleAfterMiddleware
{
    public function __invoke(Request $request, RequestHandler $handler): Response
    {
        // Invoke the next middleware and get response
        $response = $handler->handle($request);

        // Handle the outgoing response
        // ...

        return $response;
    }
}

PSR-15 中介層

PSR-15 是一個標準,它定義了 HTTP 伺服器要求處理常式和中介層元件的常見介面。

Slim 提供 PSR-15 中介層的內建支援。

主要的介面

  • Psr\Http\Server\MiddlewareInterface:這個介面定義了中介層必須實作的 process 方法。
  • Psr\Http\Server\RequestHandlerInterface:一個 HTTP 要求處理常式,會處理一個 HTTP 要求以產生一個 HTTP 響應。

要建立一個 PSR-15 中介層類別,您需要實作 MiddlewareInterface

下方是一個 PSR-15 中介層的簡單範例

<?php

namespace App\Middleware;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class ExampleMiddleware implements MiddlewareInterface
{
    public function process(Request $request, RequestHandler $handler): Response
    {
        // Optional: Handle the incoming request
        // ...

        // Invoke the next middleware and get response
        $response = $handler->handle($request);

        // Optional: Handle the outgoing response
        // ...

        return $response;
    }
}

可以認證、授權、記錄、驗證或修改進入的要求。

可以記錄、轉換、壓縮或新增其他標頭給傳出的回應。

在 PSR-15 中介層中建立一個新的回應

要建立一個新的回應,請使用 Psr\Http\Message\ResponseFactoryInterface,它提供一個 createResponse() 方法來建立一個新的回應物件。

以下是建立一個新的回應之 PSR-15 中介層類別範例

<?php

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ExampleMiddleware implements MiddlewareInterface
{
    private ResponseFactoryInterface $responseFactory;

    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // Check some condition to determine if a new response should be created
        if (true) {
            // Create a new response using the response factory
            $response = $this->responseFactory->createResponse();
            $response->getBody()->write('New response created by middleware');
            
            return $response;
        }

        // Proceed with the next middleware
        return $handler->handle($request);
    }
}

預設的情況下,回應是建立於 200 OK 狀態代碼。若要變更 HTTP 狀態代碼,您可以傳遞所需的狀態代碼作為參數給 createResponse 方法。

$response = $this->responseFactory->createResponse(201);

註記:回應工廠是一個必須注入到中介層中的依賴性。請確定 Slim
DI 容器(如 PHP-DI)經過妥善設定,可提供 Psr\Http\Message\ResponseFactoryInterface 執行個體。

範例:使用 slim\psr7 套件的 PHP-DI 定義

use Psr\Container\ContainerInterface;
use Slim\Psr7\Factory\ResponseFactory;
// ...

return [
    // ...
    ResponseFactoryInterface::class => function (ContainerInterface $container) {
        return $container->get(ResponseFactory::class);
    },
];

範例:使用 nyholm/psr7 套件的 PHP-DI 定義

use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Container\ContainerInterface;
// ...

return [
    // ...
    ResponseFactoryInterface::class => function (ContainerInterface $container) {
        return $container->get(Psr17Factory::class);
    },
];

註冊中介軟體

若要使用中介軟體,您需要在 Slim $app、路由或路由群組上註冊各個中介軟體。

// Add middleware to the App
$app->add(new ExampleMiddleware());

// Add middleware to the App using dependency injection
$app->add(ExampleMiddleware::class);

// Add middleware to a route
$app->get('/', function () { ... })->add(new ExampleMiddleware());

// Add middleware to a route group
$app->group('/', function () { ... })->add(new ExampleMiddleware());

中介軟體執行順序

Slim 會遵循 Last In, First Out (LIFO) 順序處理中介軟體。這表示最後新增的中介軟體會是第一個執行的中介軟體。如果您新增多個中介軟體元件,他們將會以與新增順序相反的順序執行。

$app->add(new MiddlewareOne());
$app->add(new MiddlewareTwo());
$app->add(new MiddlewareThree());

在這個範例中,MiddlewareThree 會先執行,其次是 MiddlewareTwo,接著才是 MiddlewareOne

路由中介軟體

只有在其路由與目前的 HTTP 請求方法和 URI 相符時,才會呼叫路由中介軟體。路由中介軟體會在呼叫 Slim 應用程式的任何路由方法後(如 get()post())立即指定。每個路由方法會回傳 \Slim\Route 的執行個體,而這個類別會提供與 Slim 應用程式執行個體相同的介面。使用 Route 執行個體的 add() 方法新增中介軟體到 Route。這個範例新增了上述的 Closure 中介軟體範例

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$middleware = function (Request $request, RequestHandler $handler) {
    $response = $handler->handle($request);
    $response->getBody()->write('World');

    return $response;
};

$app->get('/', function (Request $request, Response $response) {
    $response->getBody()->write('Hello ');

    return $response;
})->add($middleware);

$app->run();

這會輸出這個 HTTP 回應主體

Hello World

群組中介軟體

除了整體應用程式和標準路由能夠接受中介軟體之外,group() 多路由定義功能也允許個別路由在內部處理。只有在路由與群組中任一已定義的 HTTP 請求方法和 URI 相符時,才會呼叫群組中介軟體。若要在回呼中新增中介軟體和整體群組中介軟體,可以在 group() 方法後串接 add() 進行設定。

範例應用程式,示範在一個 URL 處理程式群組上使用回呼中介軟體。

<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->get('/', function (Request $request, Response $response) {
    $response->getBody()->write('Hello World');
    return $response;
});

$app->group('/utils', function (RouteCollectorProxy $group) {
    $group->get('/date', function (Request $request, Response $response) {
        $response->getBody()->write(date('Y-m-d H:i:s'));
        return $response;
    });
    
    $group->get('/time', function (Request $request, Response $response) {
        $response->getBody()->write((string)time());
        return $response;
    });
})->add(function (Request $request, RequestHandler $handler) use ($app) {
    $response = $handler->handle($request);
    $dateOrTime = (string) $response->getBody();

    $response = $app->getResponseFactory()->createResponse();
    $response->getBody()->write('It is now ' . $dateOrTime . '. Enjoy!');

    return $response;
});

$app->run();

在呼叫 /utils/date 方法時,會輸出一個類似以下的字串。

It is now 2015-07-06 03:11:01. Enjoy!

瀏覽 /utils/time 會輸出一個類似以下的字串。

It is now 1436148762. Enjoy!

但瀏覽 / (網域名稱根目錄)預計會產生以下輸出,因為沒有指定任何中介軟體。

Hello World

從中介軟體傳遞變數

從中介軟體傳遞屬性的最簡單方式是使用請求的屬性。

在中介軟體中設定變數

$request = $request->withAttribute('foo', 'bar');

在路由回呼中取得變數

$foo = $request->getAttribute('foo');

尋找可用的中介軟體

您或許會找到一個已經撰寫好且能滿足您需求的 PSR-15 中介軟體類別。以下列出幾個非官方清單供您搜尋。