中间件

您可以在 Slim 应用程序之前之后运行代码,以适当地操作请求和响应对象。这种操作称为中间件。为什么要这样做?也许您希望保护您的应用程序免受跨站点请求伪造的影响。也许您希望在您的应用程序运行之前对请求进行身份验证。中间件非常适合这些场景。

什么是中间件?

中间件是 Web 应用程序中位于客户端请求和服务器响应之间的层。当 HTTP 请求和响应通过应用程序管道时,中间件会拦截、处理并可能更改这些请求和响应。

中间件可以处理各种任务,例如身份验证、授权、日志记录、请求修改、响应转换、错误处理等。

每个中间件执行其功能,然后将控制权传递给链中的下一个中间件,从而在 Web 应用程序中实现一种模块化且可重用的方法以处理横切关注点。

中间件如何工作?

不同的框架对中间件的使用方式不同。Slim 将中间件添加为围绕核心应用程序的同心层。每个新中间件层都会包围任何现有的中间件层。随着添加额外的中间件层,同心结构会向外扩展。

添加的最后一个中间件层是最先执行的层。

您运行 Slim 应用程序时,请求对象由外向内遍历中间件结构。它们先进入最外面的中间件,然后进入下一个最外面的中间件(依此类推),最后才能到达 Slim 应用程序本身。Slim 应用程序调度相应的路由之后,响应结果对象将退出 Slim 应用程序,并由内向外遍历中间件结构。最终,最终的响应对象将退出最外面的中间件,序列化为原始的 HTTP 响应并返回到 HTTP 客户端。下面是一个阐明中间件流程的图表

Middleware architecture

如何编写中间件?

中间件是可以接受两个参数的回调函数:一个 请求 对象和一个 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 按后进先出 (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() 方法将中间件添加到路由。此示例添加了上述闭包中间件示例

<?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 中间件类来满足你的需求。以下是几个非官方的搜索列表。