跳到内容

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 行代码。

本作品,包括代码示例,均在 Creative Commons BY-SA 3.0 许可下获得许可。
目录
    版本