HttpKernel 组件:控制器解析器
您可能会认为我们的框架已经非常完善,您可能是对的。但让我们看看如何进一步改进它。
目前,我们所有的示例都使用过程式代码,但请记住控制器可以是任何有效的 PHP 回调。让我们将控制器转换为一个合适的类
1 2 3 4 5 6 7 8 9 10 11
class LeapYearController
{
public function index($request): Response
{
if (is_leap_year($request->attributes->get('year'))) {
return new Response('Yep, this is a leap year!');
}
return new Response('Nope, this is not a leap year.');
}
}
相应地更新路由定义
1 2 3 4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [
'year' => null,
'_controller' => [new LeapYearController(), 'index'],
]));
这个改动非常直接,并且在您创建更多页面时非常有意义,但您可能已经注意到一个不希望出现的副作用... LeapYearController
类*总是*被实例化,即使请求的 URL 与 leap_year
路由不匹配。这有一个主要缺点:从性能方面来看,所有路由的所有控制器现在都必须为每个请求实例化。如果控制器是延迟加载的,以便只实例化与匹配路由关联的控制器,那就更好了。
为了解决这个问题以及更多问题,让我们安装并使用 HttpKernel 组件
1
$ composer require symfony/http-kernel
HttpKernel 组件有许多有趣的功能,但我们现在需要的是*控制器解析器*和*参数解析器*。控制器解析器知道如何确定要执行的控制器,而参数解析器基于 Request 对象确定要传递给控制器的参数。所有控制器解析器都实现了以下接口
1 2 3 4 5 6 7
namespace Symfony\Component\HttpKernel\Controller;
// ...
interface ControllerResolverInterface
{
public function getController(Request $request);
}
getController()
方法依赖于与我们之前定义的约定相同的约定:_controller
请求属性必须包含与 Request 关联的控制器。除了内置的 PHP 回调,getController()
还支持由类名、两个冒号和方法名组成的字符串作为有效回调,例如 'class::method'
1 2 3 4
$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', [
'year' => null,
'_controller' => 'LeapYearController::index',
]));
为了使这段代码工作,修改框架代码以使用来自 HttpKernel 的控制器解析器
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpKernel;
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);
注意
作为额外的好处,控制器解析器会为您妥善处理错误管理:例如,当您忘记为 Route 定义 _controller
属性时。
现在,让我们看看控制器参数是如何被推断出来的。getArguments()
通过使用原生 PHP 反射 内省控制器签名以确定要传递给它的参数。此方法在以下接口中定义
1 2 3 4 5 6 7
namespace Symfony\Component\HttpKernel\Controller;
// ...
interface ArgumentResolverInterface
{
public function getArguments(Request $request, $controller);
}
index()
方法需要 Request 对象作为参数。如果类型提示正确,getArguments()
知道何时正确注入它
1 2 3 4
public function index(Request $request)
// won't work
public function index($request)
更令人感兴趣的是,getArguments()
也能够注入任何 Request 属性;如果参数的名称与相应的属性相同
1
public function index(int $year)
您也可以同时注入 Request 和一些属性(由于匹配是根据参数名称或类型提示完成的,因此参数顺序无关紧要)
1 2 3
public function index(Request $request, int $year)
public function index(int $year, Request $request)
最后,您还可以为任何与 Request 的可选属性匹配的参数定义默认值
1
public function index(int $year = 2012)
让我们为我们的控制器注入 $year
请求属性
1 2 3 4 5 6 7 8 9 10 11
class LeapYearController
{
public function index(int $year): Response
{
if (is_leap_year($year)) {
return new Response('Yep, this is a leap year!');
}
return new Response('Nope, this is not a leap year.');
}
}
解析器还负责验证控制器可调用对象及其参数。如果出现问题,它会抛出一个异常,并附带一条友好的消息来解释问题(控制器类不存在,方法未定义,参数没有匹配的属性,...)。
注意
凭借默认控制器解析器和参数解析器的强大灵活性,您可能会想知道为什么有人会想要创建另一个解析器(如果没有接口,为什么要创建接口?)。 两个例子:在 Symfony 中,getController()
得到增强以支持 控制器作为服务;getArguments()
提供了一个扩展点来更改或增强参数的解析。
让我们用新版本的框架来总结
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 39 40 41
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;
function render_template(Request $request): Response
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
return new Response(ob_get_clean());
}
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
$response->send();
再仔细想想:我们的框架比以往任何时候都更健壮、更灵活,而且仍然只有不到 50 行代码。