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 应用程序中请求-响应生命周期的简化概述
- 用户在 浏览器 中请求 资源;
- 浏览器 向 服务器 发送 请求;
- Symfony 为 应用程序 提供一个 Request 对象;
- 应用程序 使用 Request 对象的数据生成一个 Response 对象;
- 服务器 将 响应 发送回 浏览器;
- 浏览器 向 用户 显示 资源。
通常,会构建某种框架或系统来处理所有重复性任务(例如路由、安全等),以便开发人员可以构建应用程序的每个页面。这些系统如何构建差异很大。 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
事件设置响应时,传播将停止。这意味着优先级较低的监听器将不会被执行。
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 可调用对象(控制器)。
3) kernel.controller
事件
典型用途:在控制器执行之前初始化事物或更改控制器。
在确定控制器可调用对象后,HttpKernel::handle()
会调度 kernel.controller
事件。此事件的监听器可能会初始化需要在某些内容确定后(例如控制器、路由信息)但在控制器执行之前初始化的系统部分。
此事件的另一个典型用例是使用 getAttributes() 方法从控制器检索属性。有关一些示例,请参阅下面的 Symfony 部分。
此事件的监听器还可以通过调用传递给此事件监听器的事件对象上的 ControllerEvent::setController 来完全更改控制器可调用对象。
4) 获取控制器参数
接下来,HttpKernel::handle()
调用 ArgumentResolverInterface::getArguments()。请记住,在 getController()
中返回的控制器是可调用对象。getArguments()
的目的是返回应传递给该控制器的参数数组。究竟如何完成完全取决于您的设计,尽管内置的 ArgumentResolver 是一个很好的例子。
此时,内核具有 PHP 可调用对象(控制器)和执行该可调用对象时应传递的参数数组。
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
事件设置响应时,传播将被停止。这意味着优先级较低的监听器将不会被执行。
7) kernel.response
事件
典型用途:在 Response
对象发送之前对其进行修改
内核的最终目标是将 Request
转换为 Response
。Response
可能在 kernel.request 事件期间创建,从 控制器 返回,或者由 kernel.view 事件的监听器之一返回。
无论谁创建了 Response
,另一个事件 - kernel.response
都会在此之后立即被调度。此事件的典型监听器将以某种方式修改 Response
对象,例如修改标头、添加 cookie,甚至更改 Response
本身的内容(例如,在 HTML 响应的结束 </body>
标记之前注入一些 JavaScript)。
在此事件被调度之后,最终的 Response
对象将从 handle() 返回。在最典型的用例中,您可以随后调用 send() 方法,该方法发送标头并打印 Response
内容。
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
事件设置响应时,传播将被停止。这意味着优先级较低的监听器将不会被执行。
创建事件监听器
如您所见,您可以创建事件监听器并将其附加到 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');