跳到内容

HttpKernel 组件

编辑此页

HttpKernel 组件提供了一个结构化的流程,通过使用 EventDispatcher 组件将 Request 转换为 Response。它足够灵活,可以创建一个全栈框架 (Symfony) 或一个高级 CMS (Drupal)。

安装

1
$ composer require symfony/http-kernel

注意

如果您在 Symfony 应用程序之外安装此组件,您必须在代码中引入 vendor/autoload.php 文件,以启用 Composer 提供的类自动加载机制。阅读 这篇文章 了解更多详情。

请求-响应生命周期

另请参阅

本文解释了如何在任何 PHP 应用程序中将 HttpKernel 功能用作独立组件。在 Symfony 应用程序中,一切都已配置好,随时可以使用。阅读 控制器事件和事件监听器 文章,了解如何在 Symfony 应用程序中使用它来创建控制器和定义事件。

每个 HTTP Web 交互都始于请求,终于响应。作为开发人员,您的工作是创建 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 对象有一个 "attributes" 包,它是存储有关请求的额外、应用程序特定数据的理想位置。这意味着,如果您的路由器监听器以某种方式确定了控制器,它可以将其存储在 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 对象的 attributes 属性上查找 _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.response 事件。

但是,如果控制器返回除 Response 之外的任何内容,则内核还有更多工作要做 - kernel.view(因为最终目标始终是生成 Response 对象)。

注意

控制器必须返回某些内容。如果控制器返回 null,将立即抛出异常。

6) kernel.view 事件

典型用途:将控制器的非 Response 返回值转换为 Response

内核事件信息表

如果控制器没有返回 Response 对象,那么内核会调度另一个事件 - kernel.view。此事件监听器的任务是使用控制器的返回值(例如,数据数组或对象)来创建一个 Response

如果您想使用“视图”层,这将非常有用:您可以返回代表页面的数据,而不是从控制器返回 Response。此事件的监听器随后可以使用此数据来创建一个格式正确(例如 HTML、JSON 等)的 Response

在这个阶段,如果没有监听器在此事件上设置响应,则会抛出一个异常:控制器视图监听器之一必须始终返回一个 Response

注意

当为 kernel.view 事件设置响应时,传播将被停止。这意味着优先级较低的监听器将不会被执行。

Symfony 框架内部有一个用于 kernel.view 事件的默认监听器。如果您的控制器操作返回一个数组,并且您将 #[Template] 属性 应用于该控制器操作,则此监听器将渲染一个模板,将您从控制器返回的数组传递给该模板,并创建一个包含该模板返回内容的 Response

此外,一个流行的社区 bundle FOSRestBundle 在此事件上实现了一个监听器,旨在为您提供一个强大的视图层,能够使用单个控制器返回多种不同内容类型的响应(例如 HTML、JSON、XML 等)。

7) kernel.response 事件

典型用途:在 Response 对象发送之前对其进行修改

内核事件信息表

内核的最终目标是将 Request 转换为 ResponseResponse 可能在 kernel.request 事件期间创建,从 控制器 返回,或者由 kernel.view 事件的监听器之一返回。

无论谁创建了 Response,另一个事件 - kernel.response 都会在此之后立即被调度。此事件的典型监听器将以某种方式修改 Response 对象,例如修改标头、添加 cookie,甚至更改 Response 本身的内容(例如,在 HTML 响应的结束 </body> 标记之前注入一些 JavaScript)。

在此事件被调度之后,最终的 Response 对象将从 handle() 返回。在最典型的用例中,您可以随后调用 send() 方法,该方法发送标头并打印 Response 内容。

Symfony 框架内部在此事件上存在几个次要的监听器,并且大多数都以某种方式修改响应。例如,WebDebugToolbarListenerdev 环境中将一些 JavaScript 注入到页面底部,这会导致显示 web debug toolbar。另一个监听器 ContextListener 将当前用户的信息序列化到 session 中,以便在下次请求时可以重新加载。

8) kernel.terminate 事件

典型用途:在响应流式传输到用户后执行一些“繁重”的操作

内核事件信息表

HttpKernel 进程的最后一个事件是 kernel.terminate,它是独特的,因为它发生在 HttpKernel::handle() 方法之后,以及在响应发送给用户之后。回想一下上面的内容,然后使用内核的代码像这样结束

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 事件是可选的,并且仅应在您的内核实现 TerminableInterface 时调用。

9) 处理异常:kernel.exception 事件

典型用途:处理某种类型的异常并创建一个适当的 Response 以返回异常

内核事件信息表

如果在 HttpKernel::handle() 内部的任何时候抛出异常,则会调度另一个事件 - kernel.exception。在内部,handle() 方法的主体被包裹在一个 try-catch 块中。当抛出任何异常时,kernel.exception 事件被调度,以便您的系统可以以某种方式响应异常。

此事件的每个监听器都会传递一个 ExceptionEvent 对象,您可以使用该对象通过 getThrowable() 方法访问原始异常。此事件的典型监听器将检查某种类型的异常,并创建一个适当的错误 Response

例如,要生成 404 页面,您可能会抛出一种特殊类型的异常,然后在此事件上添加一个监听器,该监听器查找此异常并创建并返回一个 404 Response。实际上,HttpKernel 组件带有一个 ErrorListener,如果您选择使用它,默认情况下会执行此操作以及更多操作(请参阅下面的侧边栏以获取更多详细信息)。

ExceptionEvent 公开了 isKernelTerminating() 方法,您可以使用该方法来确定内核在抛出异常时是否正在终止。

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 应用程序中使用的 bundle 机制。bundle 的关键功能之一是您可以使用逻辑路径而不是物理路径来引用它们的任何资源(配置文件、模板、控制器、翻译文件等)。

即使您不知道 bundle 将安装在文件系统中的哪个位置,这也允许导入资源。例如,存储在名为 FooBundle 的 bundle 的 Resources/config/ 目录中的 services.xml 文件可以被引用为 @FooBundle/Resources/config/services.xml,而不是 __DIR__/Resources/config/services.xml

这要归功于内核提供的 locateResource() 方法,该方法将逻辑路径转换为物理路径

1
$path = $kernel->locateResource('@FooBundle/Resources/config/services.xml');
这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本